| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214 |
- """
- 灵感点分析结果可视化脚本
- 读取 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]]:
- """
- 加载所有灵感点的分析结果
- Args:
- inspiration_dir: 灵感点目录路径
- Returns:
- 灵感点分析结果列表
- """
- inspiration_path = Path(inspiration_dir)
- results = []
- # 遍历所有子目录
- for subdir in inspiration_path.iterdir():
- if subdir.is_dir():
- # 查找 all_summary 文件
- summary_files = list(subdir.glob("all_summary_*.json"))
- if summary_files:
- summary_file = summary_files[0]
- try:
- with open(summary_file, 'r', encoding='utf-8') as f:
- data = json.load(f)
- # 加载完整的 step1 和 step2 数据
- step1_data = None
- step2_data = None
- # 直接从当前子目录查找 step1 和 step2 文件
- step1_files = list(subdir.glob("all_step1_*.json"))
- step2_files = list(subdir.glob("all_step2_*.json"))
- if step1_files:
- try:
- with open(step1_files[0], 'r', encoding='utf-8') as f:
- step1_data = json.load(f)
- except Exception as e:
- print(f"警告: 读取 {step1_files[0]} 失败: {e}")
- if step2_files:
- try:
- with open(step2_files[0], 'r', encoding='utf-8') as f:
- step2_data = json.load(f)
- except Exception as e:
- print(f"警告: 读取 {step2_files[0]} 失败: {e}")
- # 加载搜索结果和匹配分数
- search_results = {}
- search_dir = subdir / "search"
- if search_dir.exists() and search_dir.is_dir():
- search_files = list(search_dir.glob("all_search_*.json"))
- for search_file in search_files:
- try:
- with open(search_file, 'r', encoding='utf-8') as f:
- search_data = json.load(f)
- # 从JSON内容中读取真实的keyword,而不是从文件名提取
- keyword = search_data.get("search_params", {}).get("keyword", "")
- if keyword:
- # 尝试加载对应的匹配结果文件
- match_file = search_dir / "all_step4_搜索结果匹配_gemini-2.5-pro.json"
- match_data = None
- if match_file.exists():
- try:
- with open(match_file, 'r', encoding='utf-8') as mf:
- match_data = json.load(mf)
- except Exception as e:
- print(f"警告: 读取匹配文件 {match_file} 失败: {e}")
- search_results[keyword] = {
- "search_data": search_data,
- "match_data": match_data
- }
- else:
- # 如果JSON中没有keyword,则从文件名提取
- keyword = search_file.stem.replace("all_search_", "")
- search_results[keyword] = {
- "search_data": search_data,
- "match_data": None
- }
- except Exception as e:
- print(f"警告: 读取 {search_file} 失败: {e}")
- results.append({
- "summary": data,
- "step1": step1_data,
- "step2": step2_data,
- "search_results": search_results,
- "inspiration_name": subdir.name
- })
- except Exception as e:
- print(f"警告: 读取 {summary_file} 失败: {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_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-post", post_to_mapping_data: Dict[str, Any] = None) -> str:
- """
- 生成单个帖子卡片HTML(与搜索结果样式一致)
- Args:
- post: 帖子数据
- note_id_prefix: 帖子ID前缀,用于区分不同区域的帖子
- post_to_mapping_data: 帖子到分类和点映射数据
- Returns:
- HTML字符串
- """
- import html as html_module
- import random
- title = post.get("title", "")
- desc = post.get("body_text", "")
- images = post.get("images", [])
- like_count = post.get("like_count", 0)
- comment_count = post.get("comment_count", 0)
- link = post.get("link", "")
- author = post.get("channel_account_name", "")
- post_id = post.get("channel_content_id", "")
- publish_time = post.get("publish_time", "")
- # 生成唯一的note_id
- note_id = f"{note_id_prefix}-{random.randint(10000, 99999)}"
- # 生成图片轮播HTML
- images_html = ""
- if images and len(images) > 0:
- images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
- # 图片导航按钮
- nav_buttons = ""
- if len(images) > 1:
- nav_buttons = f'''
- <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
- <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
- '''
- images_html = f'''
- <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
- <div class="note-images-track" id="{note_id}-track">
- {images_track}
- </div>
- {nav_buttons}
- </div>
- '''
- else:
- # 无图片时显示占位符
- images_html = f'''
- <div class="note-image-carousel">
- <div class="note-images-track">
- <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
- 暂无图片
- </div>
- </div>
- </div>
- '''
- # 准备详情数据
- note_data = {
- "title": title,
- "desc": desc,
- "images": images,
- "link": link,
- "author": author,
- "like_count": like_count,
- "comment_count": comment_count
- }
- # 添加灵感点、关键点、目的点到note_data
- if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
- mapping = post_to_mapping_data[post_id]
- note_data["inspiration_points"] = mapping.get("灵感点列表", [])
- note_data["key_points"] = mapping.get("关键点列表", [])
- note_data["purpose_points"] = mapping.get("目的点列表", [])
- import json
- note_data_json = json.dumps(note_data, ensure_ascii=False)
- note_data_json_escaped = html_module.escape(note_data_json)
- # 获取帖子的灵感点、关键点、目的点
- points_html = ""
- if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
- mapping = post_to_mapping_data[post_id]
- inspiration_points = mapping.get("灵感点列表", [])
- key_points = mapping.get("关键点列表", [])
- purpose_points = mapping.get("目的点列表", [])
- points_sections = []
- # 灵感点
- if inspiration_points:
- insp_items = "".join([
- f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("灵感点", ""))}</span></div>'
- for p in inspiration_points[:3] # 最多显示3个
- ])
- points_sections.append(f'<div class="points-section"><div class="points-label">灵感点</div><div class="points-list">{insp_items}</div></div>')
- # 关键点
- if key_points:
- key_items = "".join([
- f'<div class="point-item"><span class="point-name">{html_module.escape(k.get("关键点", ""))}</span></div>'
- for k in key_points[:3] # 最多显示3个
- ])
- points_sections.append(f'<div class="points-section"><div class="points-label">关键点</div><div class="points-list">{key_items}</div></div>')
- # 目的点
- if purpose_points:
- purpose_items = "".join([
- f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("目的点", ""))}</span></div>'
- for p in purpose_points[:3] # 最多显示3个
- ])
- points_sections.append(f'<div class="points-section"><div class="points-label">目的点</div><div class="points-list">{purpose_items}</div></div>')
- if points_sections:
- points_html = f'<div class="note-points">{"".join(points_sections)}</div>'
- # 生成发布日期HTML
- publish_date_html = ""
- if publish_time:
- publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
- card_html = f'''
- <div class="search-note-item" data-note-data='{note_data_json_escaped}' onclick="showNoteDetail(this)">
- {images_html}
- <div class="note-content">
- <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
- <div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
- {publish_date_html}
- <div class="note-footer">
- <div class="note-author">@{html_module.escape(author) if author else "匿名"}</div>
- <div class="note-stats">
- <span>👍 {like_count if like_count else 0}</span>
- {f'<span>💬 {comment_count}</span>' if comment_count else ''}
- </div>
- </div>
- </div>
- {f'<div class="note-points-hover">{points_html}</div>' if points_html else ''}
- </div>
- '''
- return card_html
- def generate_inspiration_card_html(
- inspiration_data: Dict[str, Any],
- inspiration_to_post_data: Dict[str, Any] = None,
- category_index_data: Dict[str, Any] = None,
- post_to_mapping_data: Dict[str, Any] = None
- ) -> str:
- """
- 生成单个灵感点的卡片HTML
- Args:
- inspiration_data: 灵感点数据
- inspiration_to_post_data: 点到帖子映射数据
- category_index_data: 分类索引数据
- Returns:
- HTML字符串
- """
- summary = inspiration_data.get("summary", {})
- step1 = inspiration_data.get("step1", {})
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- search_results = inspiration_data.get("search_results", {})
- # 提取关键指标
- metrics = summary.get("关键指标", {})
- step1_score = metrics.get("step1_top1_score", 0)
- step1_match_element = metrics.get("step1_top1_匹配要素", "")
- # 确定卡片颜色(基于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匹配结果(简要展示)
- step1_matches = step1.get("匹配结果列表", []) if step1 else []
- step1_match_preview = ""
- if step1_matches:
- top_match = step1_matches[0]
- # 从新的数据结构中提取信息
- input_info = top_match.get("输入信息", {})
- match_result = top_match.get("匹配结果", {})
- element_name = input_info.get("A", "")
- match_score = match_result.get("score", 0)
- same_parts = match_result.get("相同部分", {}) or {}
- increment_parts = match_result.get("增量部分", {}) or {}
- # 生成相同部分和增量部分的HTML
- parts_html = ""
- if same_parts:
- same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()]
- parts_html += f'<div class="preview-parts same"><strong>相同:</strong> {", ".join(same_items)}</div>'
- if increment_parts:
- inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()]
- parts_html += f'<div class="preview-parts increment"><strong>增量:</strong> {", ".join(inc_items)}</div>'
- step1_match_preview = f'''
- <div class="match-preview">
- <div class="match-preview-header">🎯 Step1 Top1匹配</div>
- <div class="match-preview-content">
- <span class="match-preview-name">{html_module.escape(element_name)}</span>
- <span class="match-preview-score" style="color: {step1_color};">{match_score:.2f}</span>
- </div>
- {parts_html}
- </div>
- '''
- # 提取 top3 匹配信息
- top3_matches = []
- if step1:
- matches = step1.get("匹配结果列表", [])
- for i, match in enumerate(matches[:3]):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- element_name = input_info.get("A", "")
- match_score = match_result.get("score", 0)
- context = input_info.get("A_Context", "")
- # 解析层级关系
- hierarchy = []
- if context:
- lines = context.split("\n")
- for line in lines:
- if ":" in line:
- key, value = line.split(":", 1)
- key = key.strip()
- value = value.strip()
- if key in ["所属视角", "一级分类", "二级分类"]:
- hierarchy.append(value)
- top3_matches.append({
- "rank": i + 1,
- "name": element_name,
- "score": match_score,
- "hierarchy": hierarchy
- })
- # 准备详细数据用于弹窗
- detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
- detail_data_json_escaped = html_module.escape(detail_data_json)
- # top3 匹配数据
- top3_json = json.dumps(top3_matches, ensure_ascii=False)
- top3_json_escaped = html_module.escape(top3_json)
- # 生成详细HTML并进行HTML转义
- detail_html = generate_detail_html(inspiration_data)
- detail_html_escaped = html_module.escape(detail_html)
- # 生成匹配列表HTML
- matches_html = ""
- if step1:
- step1_matches = step1.get("匹配结果列表", [])
- for idx, match in enumerate(step1_matches):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- element_name = input_info.get("A", "")
- context = input_info.get("A_Context", "")
- score = match_result.get("score", 0)
- score_explain = match_result.get("score说明", "") or ""
- same_parts = match_result.get("相同部分", {}) or {}
- increment_parts = match_result.get("增量部分", {}) or {}
- # 为搜索结果容器生成唯一ID
- safe_insp_name = ''.join(c if c.isalnum() else '_' for c in inspiration_name)
- unique_match_id = f"{safe_insp_name}-match-{idx}"
- # 解析层级
- hierarchy = []
- if context:
- lines = context.split("\n")
- for line in lines:
- if ":" in line:
- key, value = line.split(":", 1)
- key = key.strip()
- value = value.strip()
- if key in ["所属视角", "一级分类", "二级分类"]:
- hierarchy.append(value)
- hierarchy_html = " › ".join([html_module.escape(h) for h in hierarchy]) if hierarchy else ""
- rank_class = f"rank-{idx + 1}" if idx < 3 else ""
- # 相同部分HTML
- same_parts_html = ""
- if same_parts:
- same_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in same_parts.items()])
- same_parts_html = f'''
- <div class="match-parts same-parts">
- <div class="match-parts-title">✅ 相同部分</div>
- {same_items}
- </div>
- '''
- # 增量部分HTML
- increment_parts_html = ""
- if increment_parts:
- inc_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in increment_parts.items()])
- increment_parts_html = f'''
- <div class="match-parts increment-parts">
- <div class="match-parts-title">➕ 增量部分</div>
- {inc_items}
- </div>
- '''
- # 生成搜索结果HTML(网格展示,图片轮播)
- search_html = ""
- if element_name in search_results:
- result_obj = search_results[element_name]
- search_data = result_obj.get("search_data", {})
- match_data = result_obj.get("match_data", None)
- search_params = search_data.get("search_params", {})
- notes = search_data.get("notes", [])
- notes_count = len(notes)
- # 构建匹配分数字典 {channel_content_id: match_info}
- match_scores = {}
- if match_data and "匹配结果列表" in match_data:
- for match_item in match_data["匹配结果列表"]:
- business_info = match_item.get("业务信息", {})
- input_info = match_item.get("输入信息", {})
- content_id = business_info.get("channel_content_id", "")
- if content_id:
- match_scores[content_id] = {
- "score": match_item.get("匹配结果", {}).get("score", 0),
- "score说明": match_item.get("匹配结果", {}).get("score说明", "") or "",
- "相同部分": match_item.get("匹配结果", {}).get("相同部分", {}) or {},
- "增量部分": match_item.get("匹配结果", {}).get("增量部分", {}) or {},
- "输入B": input_info.get("B", "") or "",
- "输入A": input_info.get("A", "") or "",
- "B_Context": input_info.get("B_Context", "") or "",
- "A_Context": input_info.get("A_Context", "") or ""
- }
- # 为notes添加匹配分数、原始索引,并准备排序
- notes_with_scores = []
- for original_idx, note in enumerate(notes):
- note_id = note.get("channel_content_id", "")
- score_info = match_scores.get(note_id, None)
- notes_with_scores.append({
- "note": note,
- "score_info": score_info,
- "original_index": original_idx, # 原始搜索结果位置(0-based)
- "page": (original_idx // 20) + 1, # 第几页(假设每页20条)
- "position_in_page": (original_idx % 20) + 1 # 页内位置
- })
- # 默认按分数降序排序(没有分数的放到最后)
- notes_with_scores.sort(key=lambda x: x["score_info"]["score"] if x["score_info"] else -1, reverse=True)
- # 生成搜索参数HTML
- search_params_html = ""
- if search_params:
- keyword = search_params.get("keyword", "")
- content_type = search_params.get("content_type", "不限")
- sort_type = search_params.get("sort_type", "综合")
- publish_time = search_params.get("publish_time", "不限")
- search_params_html = f'''
- <div class="search-params-section">
- <div class="search-params-title">🔍 搜索参数</div>
- <div class="search-params-grid">
- <div class="search-param-item">
- <span class="search-param-label">关键词:</span>
- <span class="search-param-value">{html_module.escape(keyword)}</span>
- </div>
- <div class="search-param-item">
- <span class="search-param-label">内容类型:</span>
- <span class="search-param-value">{html_module.escape(content_type)}</span>
- </div>
- <div class="search-param-item">
- <span class="search-param-label">排序方式:</span>
- <span class="search-param-value">{html_module.escape(sort_type)}</span>
- </div>
- <div class="search-param-item">
- <span class="search-param-label">发布时间:</span>
- <span class="search-param-value">{html_module.escape(publish_time)}</span>
- </div>
- </div>
- </div>
- '''
- # 生成搜索结果统计HTML(带排序按钮)
- search_summary_html = f'''
- <div class="search-summary-section">
- <div class="search-summary-header">
- <div class="search-summary-left">
- <div class="search-summary-title">📊 搜索结果</div>
- <div class="search-summary-content">
- 共找到 <span class="search-result-count">{notes_count}</span> 条相关内容
- </div>
- </div>
- <div class="search-sort-buttons">
- <span class="search-sort-label">排序:</span>
- <button class="search-sort-btn active" data-sort="score" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">匹配分数</button>
- <button class="search-sort-btn" data-sort="original" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">原始顺序</button>
- <button class="search-sort-btn" data-sort="likes" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">点赞数</button>
- </div>
- </div>
- </div>
- '''
- if notes_count > 0:
- notes_items = ""
- for note_idx, item in enumerate(notes_with_scores): # 使用包含元数据的列表
- note = item["note"]
- score_info = item["score_info"]
- original_index = item["original_index"]
- page = item["page"]
- position_in_page = item["position_in_page"]
- title = note.get("title", "")
- desc = note.get("desc", "")
- link = note.get("link", "")
- author = note.get("channel_account_name", "")
- like_count = note.get("like_count", 0)
- comment_count = note.get("comment_count", 0)
- images = note.get("images", [])
- content_id = note.get("channel_content_id", "")
- publish_time = note.get("publish_time", "")
- note_id = f"note-{idx}-{note_idx}"
- # 生成图片轮播HTML
- images_html = ""
- if images and len(images) > 0:
- images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
- # 图片导航按钮和指示点
- nav_buttons = ""
- dots_html = ""
- if len(images) > 1:
- nav_buttons = f'''
- <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
- <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
- '''
- dots = "".join([f'<div class="note-image-dot{" active" if i == 0 else ""}"></div>' for i in range(len(images))])
- dots_html = f'<div class="note-image-dots" id="{note_id}-dots">{dots}</div>'
- images_html = f'''
- <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
- <div class="note-images-track" id="{note_id}-track">
- {images_track}
- </div>
- {nav_buttons}
- {dots_html}
- </div>
- '''
- else:
- # 无图片时显示占位符
- images_html = f'''
- <div class="note-image-carousel">
- <div class="note-images-track">
- <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
- 暂无图片
- </div>
- </div>
- </div>
- '''
- # 准备详情数据
- note_data = {
- "title": title,
- "desc": desc,
- "link": link,
- "author": author,
- "like_count": like_count,
- "comment_count": comment_count,
- "images": images
- }
- note_data_json = json.dumps(note_data, ensure_ascii=False)
- note_data_escaped = html_module.escape(note_data_json)
- # 生成匹配分数HTML
- score_badge_html = ""
- score_detail_html = ""
- if score_info:
- note_score = score_info["score"]
- note_score_explain = score_info.get("score说明", "") or ""
- note_same_parts = score_info.get("相同部分", {}) or {}
- note_increment_parts = score_info.get("增量部分", {}) or {}
- input_b = score_info.get("输入B", "") or ""
- input_a = score_info.get("输入A", "") or ""
- b_context = score_info.get("B_Context", "") or ""
- a_context = score_info.get("A_Context", "") or ""
- # 分数详情JSON
- score_detail_data = {
- "score": note_score,
- "score说明": note_score_explain,
- "相同部分": note_same_parts,
- "增量部分": note_increment_parts,
- "输入B": input_b,
- "输入A": input_a,
- "B_Context": b_context,
- "A_Context": a_context
- }
- score_detail_json = json.dumps(score_detail_data, ensure_ascii=False)
- score_detail_escaped = html_module.escape(score_detail_json)
- score_badge_html = f'''
- <div class="note-score-badge" onclick="event.stopPropagation(); showScoreDetail(this)" data-score-detail='{score_detail_escaped}'>
- <span class="score-label">匹配分数</span>
- <span class="score-value">{note_score:.2f}</span>
- </div>
- '''
- # 生成发布日期HTML
- publish_date_html = ""
- if publish_time:
- publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
- # 计算匹配分数(用于排序)
- sort_score = score_info["score"] if score_info else -1
- notes_items += f'''
- <div class="search-note-item"
- data-note-data='{note_data_escaped}'
- data-original-index="{original_index}"
- data-score="{sort_score}"
- data-likes="{like_count}"
- onclick="showNoteDetail(this)">
- {score_badge_html}
- {images_html}
- <div class="note-content">
- <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
- <div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
- {publish_date_html}
- <div class="note-footer">
- <div class="note-author">@{html_module.escape(author)}</div>
- <div class="note-position">P{page}-{position_in_page}</div>
- <div class="note-stats">
- <span>👍 {like_count}</span>
- <span>💬 {comment_count}</span>
- </div>
- </div>
- </div>
- </div>
- '''
- search_html = f'''
- <div class="search-results-section">
- {search_params_html}
- {search_summary_html}
- <div class="search-notes-list" id="{unique_match_id}-search-results">
- {notes_items}
- </div>
- </div>
- '''
- else:
- # 没有搜索结果时也显示参数
- search_html = f'''
- <div class="search-results-section">
- {search_params_html}
- {search_summary_html}
- </div>
- '''
- # 只有第一个匹配项默认展开
- expanded_class = " expanded" if idx == 0 else ""
- # 生成当前匹配项的灵感点和灵感分类详情
- match_info_section = ""
- if inspiration_to_post_data and category_index_data:
- # 获取灵感点数据
- inspiration_points = inspiration_to_post_data.get("点到帖子映射", {}).get("灵感点", {})
- inspiration_info = inspiration_points.get(inspiration_name, {})
- # 获取当前匹配的灵感分类数据
- categories = category_index_data.get("灵感分类", {})
- category_info = categories.get(element_name, {})
- # 生成灵感点详情HTML(左栏)
- insp_detail_html = ""
- if inspiration_info:
- insp_dimension = inspiration_info.get("维度", "")
- insp_desc = inspiration_info.get("描述", "")
- insp_posts = inspiration_info.get("帖子详情列表", [])
- # 生成帖子卡片
- insp_posts_html = ""
- for post in insp_posts[:6]: # 最多显示6个
- insp_posts_html += generate_post_card_html(post, f"{unique_match_id}-insp-post", post_to_mapping_data)
- insp_detail_html = f'''
- <div class="info-detail-column">
- <div class="info-header">
- <span class="info-label">[灵感点]</span>
- <span class="info-name">{html_module.escape(inspiration_name)}</span>
- </div>
- <div class="info-content">
- {f'<div class="info-field"><span class="info-field-label">维度:</span><span class="info-field-value">{html_module.escape(insp_dimension)}</span></div>' if insp_dimension else ''}
- {f'<div class="info-field"><span class="info-field-label">描述:</span><span class="info-field-value">{html_module.escape(insp_desc)}</span></div>' if insp_desc else ''}
- {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{insp_posts_html}</div></div>' if insp_posts_html else ''}
- </div>
- </div>
- '''
- # 生成灵感分类详情HTML(右栏)
- cat_detail_html = ""
- if category_info:
- cat_level = category_info.get("分类层级", "")
- cat_definition = category_info.get("分类定义", "")
- cat_posts = category_info.get("帖子详情列表", [])
- # 生成帖子卡片
- cat_posts_html = ""
- for post in cat_posts[:6]: # 最多显示6个
- cat_posts_html += generate_post_card_html(post, f"{unique_match_id}-cat-post", post_to_mapping_data)
- cat_detail_html = f'''
- <div class="info-detail-column">
- <div class="info-header">
- <span class="info-label">[灵感分类]</span>
- <span class="info-name">{html_module.escape(element_name)}</span>
- </div>
- <div class="info-content">
- {f'<div class="info-field"><span class="info-field-label">层级:</span><span class="info-field-value">{html_module.escape(cat_level)}</span></div>' if cat_level else ''}
- {f'<div class="info-field"><span class="info-field-label">定义:</span><span class="info-field-value">{html_module.escape(cat_definition)}</span></div>' if cat_definition else ''}
- {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{cat_posts_html}</div></div>' if cat_posts_html else ''}
- </div>
- </div>
- '''
- if insp_detail_html or cat_detail_html:
- match_info_section = f'''
- <div class="inspiration-category-section">
- {insp_detail_html}
- {cat_detail_html}
- </div>
- '''
- # 步骤1:灵感点匹配灵感分类
- step1_html = f'''
- <div class="step-section-wrapper step-1-wrapper expanded">
- <div class="step-header" onclick="toggleStepWrapper(this)">
- <div class="step-header-content">
- <span class="step-number-badge">步骤 1</span>
- <span class="step-title">灵感点匹配灵感分类</span>
- </div>
- <div class="step-toggle">▼</div>
- </div>
- <div class="step-wrapper-content">
- <div class="match-analysis-section" id="{unique_match_id}-step1" data-step-name="灵感点匹配灵感分类">
- <div class="match-parts-container">
- <div class="match-parts-column">
- {same_parts_html}
- </div>
- <div class="match-parts-column">
- {increment_parts_html}
- </div>
- </div>
- {f'<div class="match-explain"><div class="match-explain-title">💡 分数说明</div><div class="match-explain-text">{html_module.escape(score_explain)}</div></div>' if score_explain else ''}
- </div>
- </div>
- </div>
- '''
- # 步骤2:搜索结果(如果有)
- step2_html = ""
- if search_html:
- step2_html = f'''
- <div class="step-section-wrapper step-2-wrapper expanded">
- <div class="step-header" onclick="toggleStepWrapper(this)">
- <div class="step-header-content">
- <span class="step-number-badge">步骤 2</span>
- <span class="step-title">搜索</span>
- </div>
- <div class="step-toggle">▼</div>
- </div>
- <div class="step-wrapper-content">
- <div class="step-section expanded" data-step="2" id="{unique_match_id}-step2" data-step-name="灵感分类搜索">
- <div class="step-section-header" onclick="toggleStep(this)">
- <div class="step-section-title">
- <span class="step-sub-number">2.1</span>
- <span>直接搜索灵感分类</span>
- </div>
- <div class="step-toggle">▼</div>
- </div>
- <div class="step-section-content">
- {search_html}
- </div>
- </div>
- </div>
- </div>
- '''
- # 创建安全的ID(移除特殊字符)
- safe_element_id = ''.join(c if c.isalnum() or c in '_-' else '_' for c in element_name)
- matches_html += f'''
- <div class="match-item{expanded_class}" data-index="{idx}" id="{unique_match_id}" data-match-name="{html_module.escape(element_name)}">
- <div class="match-main-header" onclick="toggleMainMatch(this)">
- <div class="match-header-row">
- <div class="match-header-left">
- <span class="match-rank {rank_class}">Top {idx + 1}</span>
- <span class="detail-label">[灵感点]</span>
- <span class="match-title">{html_module.escape(inspiration_name)}</span>
- </div>
- <div class="match-header-center">
- <span class="match-score-label">匹配分数</span>
- <span class="match-score-value">{score:.2f}</span>
- </div>
- <div class="match-header-right">
- <span class="detail-label">[灵感分类]</span>
- <span class="match-category">{html_module.escape(element_name)}</span>
- <span class="match-hierarchy">({hierarchy_html})</span>
- </div>
- </div>
- <div class="match-toggle-main">▼</div>
- </div>
- <div class="match-main-content">
- {match_info_section}
- {step1_html}
- {step2_html}
- </div>
- </div>
- '''
- # 获取top1匹配的灵感分类名称(用于顶部标题)
- top1_category_name = ""
- if step1:
- step1_matches = step1.get("匹配结果列表", [])
- if step1_matches:
- top_match = step1_matches[0]
- input_info = top_match.get("输入信息", {})
- top1_category_name = input_info.get("A", "")
- html = f'''
- <div class="inspiration-detail"
- data-inspiration-name="{inspiration_name_escaped}"
- data-step1-score="{step1_score}"
- data-top3-matches="{top3_json_escaped}">
- <div class="breadcrumb-container">
- <div class="breadcrumb" id="dynamicBreadcrumb">
- <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> {inspiration_name_escaped}</span>
- </div>
- </div>
- <div class="inspiration-content-wrapper">
- <div class="matches-list">
- {matches_html}
- </div>
- </div>
- </div>
- '''
- return html
- def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成灵感点的详细信息HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- 详细信息的HTML字符串
- """
- import html as html_module
- summary = inspiration_data.get("summary", {})
- step1 = inspiration_data.get("step1", {})
- step2 = inspiration_data.get("step2", {})
- 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 = summary.get("元数据", {})
- # Step1 详细信息
- if step1 and step1.get("灵感"):
- inspiration = step1.get("灵感", "")
- matches = step1.get("匹配结果列表", [])
- content += f'''
- <div class="modal-section">
- <h3>🎯 Step1: 灵感人设匹配</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>
- '''
- # 显示匹配结果(只显示Top1)
- if matches:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">Top1匹配结果:</span>
- <div class="matches-list">
- '''
- for index, match in enumerate(matches[:1]):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- element_a = input_info.get("A", "")
- context_a = input_info.get("A_Context", "")
- score = match_result.get("score", 0)
- score_explain = match_result.get("score说明", "") or ""
- same_parts = match_result.get("相同部分", {}) or {}
- increment_parts = match_result.get("增量部分", {}) or {}
- content += f'''
- <div class="match-item">
- <div class="match-header">
- <span class="match-element-name">{html_module.escape(element_a)}</span>
- <span class="match-score">{score:.2f}</span>
- </div>
- '''
- if context_a:
- content += f'<div class="match-context"><strong>📍 所属分类:</strong> {html_module.escape(context_a).replace(chr(10), "<br>")}</div>'
- if score_explain:
- content += f'<div class="match-explain"><strong>💡 分数说明:</strong> {html_module.escape(score_explain)}</div>'
- # 相同部分
- if same_parts:
- content += '''
- <div class="match-parts same-parts">
- <div class="parts-header">✅ 相同部分</div>
- <div class="parts-content">
- '''
- for key, value in same_parts.items():
- content += f'''
- <div class="part-item">
- <span class="part-key">{html_module.escape(key)}:</span>
- <span class="part-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 增量部分
- if increment_parts:
- content += '''
- <div class="match-parts increment-parts">
- <div class="parts-header">➕ 增量部分</div>
- <div class="parts-content">
- '''
- for key, value in increment_parts.items():
- content += f'''
- <div class="part-item">
- <span class="part-key">{html_module.escape(key)}:</span>
- <span class="part-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </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_detail_modal_content_js() -> str:
- """
- 生成详情弹窗内容的JavaScript函数
- Returns:
- JavaScript代码字符串
- """
- return '''
- // 笔记图片当前索引管理
- const noteImageStates = {};
- // 移动笔记图片
- function moveNoteImage(noteId, direction) {
- if (!noteImageStates[noteId]) {
- noteImageStates[noteId] = 0;
- }
- const carousel = document.querySelector(`[data-note-id="${noteId}"]`);
- if (!carousel) return;
- const totalImages = parseInt(carousel.dataset.totalImages);
- const track = document.getElementById(noteId + '-track');
- if (!track) return;
- let newIndex = noteImageStates[noteId] + direction;
- if (newIndex < 0) newIndex = 0;
- if (newIndex >= totalImages) newIndex = totalImages - 1;
- noteImageStates[noteId] = newIndex;
- // 移动轨道
- track.style.transform = `translateX(-${newIndex * 100}%)`;
- // 更新指示点
- const dots = document.querySelectorAll(`#${noteId}-dots .note-image-dot`);
- dots.forEach((dot, i) => {
- dot.classList.toggle('active', i === newIndex);
- });
- // 更新按钮状态
- const prevBtn = carousel.querySelector('.note-carousel-button.prev');
- const nextBtn = carousel.querySelector('.note-carousel-button.next');
- if (prevBtn) {
- prevBtn.classList.toggle('disabled', newIndex === 0);
- }
- if (nextBtn) {
- nextBtn.classList.toggle('disabled', newIndex >= totalImages - 1);
- }
- }
- // 显示笔记详情
- function showNoteDetail(element) {
- const noteDataStr = element.dataset.noteData;
- if (!noteDataStr) return;
- try {
- const noteData = JSON.parse(noteDataStr);
- // 生成图片HTML
- let imagesHtml = '';
- if (noteData.images && noteData.images.length > 0) {
- imagesHtml = noteData.images.map(img =>
- `<img src="${img}" class="note-detail-image" alt="图片">`
- ).join('');
- } else {
- imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
- }
- // 生成灵感点、关键点、目的点HTML
- let pointsDetailHtml = '';
- const hasPoints = (noteData.inspiration_points && noteData.inspiration_points.length > 0) ||
- (noteData.key_points && noteData.key_points.length > 0) ||
- (noteData.purpose_points && noteData.purpose_points.length > 0);
- if (hasPoints) {
- let sections = [];
- // 灵感点
- if (noteData.inspiration_points && noteData.inspiration_points.length > 0) {
- const items = noteData.inspiration_points.map(p =>
- `<div class="detail-point-item">
- <div class="detail-point-name">${p.灵感点 || ''}</div>
- ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
- </div>`
- ).join('');
- sections.push(`<div class="detail-points-section">
- <div class="detail-points-title">💡 灵感点</div>
- ${items}
- </div>`);
- }
- // 关键点
- if (noteData.key_points && noteData.key_points.length > 0) {
- const items = noteData.key_points.map(k =>
- `<div class="detail-point-item">
- <div class="detail-point-name">${k.关键点 || ''}</div>
- ${k.描述 ? `<div class="detail-point-desc">${k.描述}</div>` : ''}
- </div>`
- ).join('');
- sections.push(`<div class="detail-points-section">
- <div class="detail-points-title">🔑 关键点</div>
- ${items}
- </div>`);
- }
- // 目的点
- if (noteData.purpose_points && noteData.purpose_points.length > 0) {
- const items = noteData.purpose_points.map(p =>
- `<div class="detail-point-item">
- <div class="detail-point-name">${p.目的点 || ''}</div>
- ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
- </div>`
- ).join('');
- sections.push(`<div class="detail-points-section">
- <div class="detail-points-title">🎯 目的点</div>
- ${items}
- </div>`);
- }
- pointsDetailHtml = `<div class="note-detail-points">${sections.join('')}</div>`;
- }
- const modalHtml = `
- <div class="note-detail-content">
- <button class="note-detail-close" onclick="closeNoteDetail()">×</button>
- <div class="note-detail-header">
- <div class="note-detail-title">${noteData.title || '无标题'}</div>
- <div class="note-detail-meta">
- <span class="note-detail-author">@${noteData.author}</span>
- <div class="note-detail-stats">
- <span>👍 ${noteData.like_count}</span>
- <span>💬 ${noteData.comment_count}</span>
- </div>
- </div>
- </div>
- <div class="note-detail-body">
- ${noteData.desc ? `<div class="note-detail-desc">${noteData.desc}</div>` : ''}
- ${pointsDetailHtml}
- <div class="note-detail-images">
- ${imagesHtml}
- </div>
- </div>
- <div class="note-detail-footer">
- <a href="${noteData.link}" target="_blank" class="note-detail-link">
- 在小红书查看完整内容 →
- </a>
- </div>
- </div>
- `;
- let modal = document.getElementById('noteDetailModal');
- if (!modal) {
- modal = document.createElement('div');
- modal.id = 'noteDetailModal';
- modal.className = 'note-detail-modal';
- modal.onclick = (e) => {
- if (e.target === modal) closeNoteDetail();
- };
- document.body.appendChild(modal);
- }
- modal.innerHTML = modalHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- } catch (e) {
- console.error('Error parsing note data:', e);
- }
- }
- // 关闭笔记详情
- function closeNoteDetail() {
- const modal = document.getElementById('noteDetailModal');
- if (modal) {
- modal.classList.remove('active');
- document.body.style.overflow = '';
- }
- }
- // 显示分数详情
- function showScoreDetail(element) {
- const scoreDetailStr = element.dataset.scoreDetail;
- if (!scoreDetailStr) return;
- try {
- const scoreData = JSON.parse(scoreDetailStr);
- const modal = document.getElementById('scoreDetailModal');
- const modalBody = document.getElementById('scoreModalBody');
- // 生成相同部分HTML
- let samePartsHTML = '';
- if (scoreData.相同部分 && Object.keys(scoreData.相同部分).length > 0) {
- const sameItems = Object.entries(scoreData.相同部分).map(([key, value]) =>
- `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
- ).join('');
- samePartsHTML = `
- <div class="score-parts same-parts">
- <div class="score-parts-title">✅ 相同部分</div>
- ${sameItems}
- </div>
- `;
- }
- // 生成增量部分HTML
- let incrementPartsHTML = '';
- if (scoreData.增量部分 && Object.keys(scoreData.增量部分).length > 0) {
- const incItems = Object.entries(scoreData.增量部分).map(([key, value]) =>
- `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
- ).join('');
- incrementPartsHTML = `
- <div class="score-parts increment-parts">
- <div class="score-parts-title">➕ 增量部分</div>
- ${incItems}
- </div>
- `;
- }
- // 生成分数说明HTML
- let explainHTML = '';
- if (scoreData.score说明) {
- explainHTML = `
- <div class="score-detail-explain">
- <div class="score-explain-title">💡 分数说明</div>
- <div class="score-explain-text">${scoreData.score说明}</div>
- </div>
- `;
- }
- // 生成输入信息HTML
- let inputInfoHTML = '';
- if (scoreData.输入B || scoreData.输入A) {
- inputInfoHTML = `
- <div class="score-input-info">
- <div class="score-input-title">📝 输入信息</div>
- ${scoreData.输入B ? `
- <div class="score-input-item">
- <div class="score-input-label">输入B(灵感点):</div>
- <div class="score-input-value">${scoreData.输入B}</div>
- </div>
- ` : ''}
- ${scoreData.B_Context ? `
- <div class="score-input-item">
- <div class="score-input-label">B_Context:</div>
- <div class="score-input-value">${scoreData.B_Context}</div>
- </div>
- ` : ''}
- ${scoreData.输入A ? `
- <div class="score-input-item">
- <div class="score-input-label">输入A(帖子标题):</div>
- <div class="score-input-value">${scoreData.输入A}</div>
- </div>
- ` : ''}
- ${scoreData.A_Context ? `
- <div class="score-input-item">
- <div class="score-input-label">A_Context(帖子内容摘要):</div>
- <div class="score-input-value">${scoreData.A_Context}</div>
- </div>
- ` : ''}
- </div>
- `;
- }
- modalBody.innerHTML = `
- <div class="score-detail-container">
- <h2 class="score-detail-title">匹配分数详情</h2>
- <div class="score-detail-score">
- <span class="score-detail-label">匹配分数:</span>
- <span class="score-detail-value">${scoreData.score.toFixed(2)}</span>
- </div>
- ${inputInfoHTML}
- ${explainHTML}
- <div class="score-parts-container">
- ${samePartsHTML}
- ${incrementPartsHTML}
- </div>
- </div>
- `;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- } catch (e) {
- console.error('Failed to parse score detail:', e);
- }
- }
- // 关闭分数详情
- function closeScoreDetail() {
- const modal = document.getElementById('scoreDetailModal');
- if (modal) {
- modal.classList.remove('active');
- document.body.style.overflow = '';
- }
- }
- // 点击Modal背景关闭分数详情
- function closeScoreDetailModal(event) {
- if (event.target.id === 'scoreDetailModal') {
- closeScoreDetail();
- }
- }
- // 搜索结果排序
- function sortSearchResults(button, containerId) {
- const sortType = button.dataset.sort;
- const container = document.getElementById(containerId);
- if (!container) return;
- // 更新按钮状态
- const allButtons = button.parentElement.querySelectorAll('.search-sort-btn');
- allButtons.forEach(btn => btn.classList.remove('active'));
- button.classList.add('active');
- // 获取所有搜索结果卡片
- const items = Array.from(container.querySelectorAll('.search-note-item'));
- // 根据排序类型排序
- items.sort((a, b) => {
- if (sortType === 'score') {
- const scoreA = parseFloat(a.dataset.score) || -1;
- const scoreB = parseFloat(b.dataset.score) || -1;
- return scoreB - scoreA; // 降序
- } else if (sortType === 'original') {
- const indexA = parseInt(a.dataset.originalIndex) || 0;
- const indexB = parseInt(b.dataset.originalIndex) || 0;
- return indexA - indexB; // 升序
- } else if (sortType === 'likes') {
- const likesA = parseInt(a.dataset.likes) || 0;
- const likesB = parseInt(b.dataset.likes) || 0;
- return likesB - likesA; // 降序
- }
- return 0;
- });
- // 重新排列DOM
- items.forEach(item => container.appendChild(item));
- }
- // ESC键关闭详情
- document.addEventListener('keydown', function(event) {
- if (event.key === 'Escape') {
- closeNoteDetail();
- closeScoreDetail();
- }
- });
- // 切换主匹配项的展开/折叠
- function toggleMainMatch(element) {
- const matchItem = element.closest('.match-item');
- matchItem.classList.toggle('expanded');
- }
- // 切换步骤wrapper的展开/折叠
- function toggleStepWrapper(element) {
- const stepWrapper = element.closest('.step-section-wrapper');
- stepWrapper.classList.toggle('expanded');
- }
- // 切换步骤的展开/折叠
- function toggleStep(element) {
- const stepSection = element.closest('.step-section');
- stepSection.classList.toggle('expanded');
- }
- // 切换匹配详情的展开/折叠
- function toggleMatchSection(element) {
- const matchSection = element.closest('.match-section');
- matchSection.classList.toggle('expanded');
- }
- // 显示指定的灵感详情
- function showDetail(index) {
- const details = document.querySelectorAll('.inspiration-detail');
- details.forEach((detail, i) => {
- if (i === index) {
- detail.classList.add('active');
- // 滚动到顶部
- const section = document.querySelector('.inspirations-section');
- if (section) {
- section.scrollTop = 0;
- }
- } else {
- detail.classList.remove('active');
- }
- });
- }
- // 生成导航目录
- function generateNavigation() {
- const details = document.querySelectorAll('.inspiration-detail');
- const navList = document.getElementById('navList');
- navList.innerHTML = '';
- details.forEach((detail, index) => {
- const name = detail.dataset.inspirationName;
- const score = parseFloat(detail.dataset.step1Score) || 0;
- const top3MatchesStr = detail.dataset.top3Matches;
- let top3Matches = [];
- try {
- top3Matches = JSON.parse(top3MatchesStr);
- } catch(e) {
- console.error('Error parsing top3 matches:', e);
- }
- const navItem = document.createElement('div');
- navItem.className = 'nav-item';
- if (index === 0) navItem.classList.add('active');
- navItem.dataset.cardIndex = index;
- // 生成匹配列表HTML
- let matchesHtml = '';
- if (top3Matches && top3Matches.length > 0) {
- matchesHtml = '<div class="nav-item-matches">';
- top3Matches.forEach((match, i) => {
- // 生成层级路径
- let hierarchyHtml = '';
- if (match.hierarchy && match.hierarchy.length > 0) {
- hierarchyHtml = '<div class="nav-match-hierarchy">';
- match.hierarchy.forEach((level, idx) => {
- if (idx > 0) {
- hierarchyHtml += '<span class="hierarchy-separator">›</span>';
- }
- hierarchyHtml += `<span class="hierarchy-item">${level}</span>`;
- });
- hierarchyHtml += '</div>';
- }
- matchesHtml += `
- <div class="nav-match-item">
- <span class="nav-match-rank rank-${match.rank}">Top${match.rank}</span>
- <div class="nav-match-content">
- <span class="nav-match-name">${match.name}</span>
- ${hierarchyHtml}
- <span class="nav-match-score">${match.score.toFixed(2)}</span>
- </div>
- </div>
- `;
- });
- matchesHtml += '</div>';
- }
- navItem.innerHTML = `
- <div class="nav-item-header">
- <span class="nav-item-name" title="${name}">
- <span class="nav-label">[灵感点]</span>${name}
- </span>
- <span class="nav-item-score">${score.toFixed(2)}</span>
- </div>
- ${matchesHtml}
- `;
- navItem.addEventListener('click', () => {
- // 移除所有active状态
- document.querySelectorAll('.nav-item').forEach(item => {
- item.classList.remove('active');
- });
- // 添加当前active状态
- navItem.classList.add('active');
- // 显示对应详情
- showDetail(index);
- });
- navList.appendChild(navItem);
- });
- // 默认显示第一个详情
- if (details.length > 0) {
- showDetail(0);
- }
- }
- // 更新面包屑
- function updateBreadcrumb(matchName, stepName) {
- const breadcrumb = document.querySelector('.inspiration-detail.active #dynamicBreadcrumb');
- if (!breadcrumb) return;
- const inspirationName = document.querySelector('.inspiration-detail.active').dataset.inspirationName;
- let breadcrumbHtml = `
- <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> ${inspirationName}</span>
- `;
- if (matchName) {
- breadcrumbHtml += `
- <span class="breadcrumb-separator">›</span>
- <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感分类]</span> ${matchName}</span>
- `;
- }
- if (stepName) {
- breadcrumbHtml += `
- <span class="breadcrumb-separator">›</span>
- <span class="breadcrumb-item breadcrumb-current">${stepName}</span>
- `;
- }
- breadcrumb.innerHTML = breadcrumbHtml;
- }
- // 监听滚动,更新面包屑
- function setupBreadcrumbObserver() {
- const activeDetail = document.querySelector('.inspiration-detail.active');
- if (!activeDetail) return;
- const contentWrapper = activeDetail.querySelector('.inspiration-content-wrapper');
- if (!contentWrapper) return;
- // 获取所有需要监听的section
- const sections = activeDetail.querySelectorAll('.step-section');
- if (sections.length === 0) return;
- // 创建Intersection Observer
- const observerOptions = {
- root: null,
- rootMargin: '-100px 0px -50% 0px',
- threshold: 0
- };
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- const section = entry.target;
- const matchItem = section.closest('.match-item');
- const matchName = matchItem ? matchItem.dataset.matchName : '';
- const stepName = section.dataset.stepName || '';
- updateBreadcrumb(matchName, stepName);
- }
- });
- }, observerOptions);
- // 观察所有section
- sections.forEach(section => observer.observe(section));
- // 存储observer以便清理
- if (!window.breadcrumbObservers) {
- window.breadcrumbObservers = [];
- }
- window.breadcrumbObservers.push(observer);
- }
- // 页面加载时生成导航
- document.addEventListener('DOMContentLoaded', () => {
- generateNavigation();
- setupBreadcrumbObserver();
- });
- // 当切换灵感点时,重新设置observer
- const originalShowDetail = showDetail;
- showDetail = function(index) {
- // 清理旧的observers
- if (window.breadcrumbObservers) {
- window.breadcrumbObservers.forEach(obs => obs.disconnect());
- window.breadcrumbObservers = [];
- }
- // 调用原始函数
- originalShowDetail(index);
- // 设置新的observer
- setTimeout(() => {
- setupBreadcrumbObserver();
- }, 100);
- };
- '''
- 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,
- inspiration_to_post_data: Dict[str, Any] = None,
- category_index_data: Dict[str, Any] = None,
- post_to_mapping_data: Dict[str, Any] = None
- ) -> 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)
- # Step1 统计
- step1_excellent_count = sum(1 for d in inspirations_data
- if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7)
- step1_good_count = sum(1 for d in inspirations_data
- if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7)
- step1_normal_count = sum(1 for d in inspirations_data
- if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5)
- step1_need_opt_count = sum(1 for d in inspirations_data
- if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3)
- # 平均分数
- total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
- for d in inspirations_data)
- avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
- # 按Step1分数排序
- inspirations_data_sorted = sorted(
- inspirations_data,
- key=lambda x: x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
- reverse=True
- )
- # 生成卡片HTML
- cards_html = [
- generate_inspiration_card_html(data, inspiration_to_post_data, category_index_data, post_to_mapping_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: #f5f7fa;
- color: #333;
- line-height: 1.6;
- margin: 0;
- padding: 0;
- overflow: hidden;
- }}
- .container {{
- max-width: 100%;
- height: 100vh;
- margin: 0;
- padding: 0;
- display: flex;
- }}
- .main-content-layout {{
- display: flex;
- width: 100%;
- height: 100vh;
- overflow: hidden;
- }}
- .sidebar-nav {{
- width: 400px;
- background: white;
- padding: 30px 20px;
- box-shadow: 2px 0 10px rgba(0,0,0,0.1);
- overflow-y: auto;
- flex-shrink: 0;
- height: 100vh;
- }}
- .nav-title {{
- font-size: 18px;
- font-weight: 700;
- color: #1a1a1a;
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .nav-list {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- }}
- .nav-item {{
- border-radius: 10px;
- cursor: pointer;
- transition: all 0.3s;
- border: 2px solid #e5e7eb;
- overflow: hidden;
- background: white;
- }}
- .nav-item:hover {{
- border-color: #667eea;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
- }}
- .nav-item.active {{
- border-color: #667eea;
- box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);
- }}
- .nav-item-header {{
- padding: 12px 14px;
- background: #f9fafb;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 8px;
- }}
- .nav-item.active .nav-item-header {{
- background: rgba(102, 126, 234, 0.1);
- }}
- .nav-item-name {{
- flex: 1;
- font-weight: 600;
- font-size: 14px;
- color: #1f2937;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- display: flex;
- align-items: center;
- gap: 6px;
- }}
- .nav-item.active .nav-item-name {{
- color: #667eea;
- }}
- .nav-label {{
- display: inline-block;
- font-size: 11px;
- color: #8b5cf6;
- background: #f5f3ff;
- padding: 2px 6px;
- border-radius: 3px;
- font-weight: 500;
- white-space: nowrap;
- flex-shrink: 0;
- }}
- .nav-item-score {{
- font-size: 13px;
- font-weight: 700;
- padding: 4px 10px;
- border-radius: 6px;
- background: #e5e7eb;
- color: #6b7280;
- }}
- .nav-item.active .nav-item-score {{
- background: #667eea;
- color: white;
- }}
- .nav-item-matches {{
- padding: 10px 14px;
- background: white;
- border-top: 1px solid #e5e7eb;
- }}
- .nav-match-item {{
- padding: 8px 0;
- font-size: 12px;
- color: #6b7280;
- display: flex;
- align-items: flex-start;
- gap: 6px;
- }}
- .nav-match-item:not(:last-child) {{
- border-bottom: 1px dashed #e5e7eb;
- }}
- .nav-match-rank {{
- font-weight: 700;
- color: #9ca3af;
- min-width: 20px;
- flex-shrink: 0;
- }}
- .nav-match-rank.rank-1 {{
- color: #f59e0b;
- }}
- .nav-match-rank.rank-2 {{
- color: #c0c0c0;
- }}
- .nav-match-rank.rank-3 {{
- color: #cd7f32;
- }}
- .nav-match-content {{
- flex: 1;
- line-height: 1.6;
- min-width: 0;
- }}
- .nav-match-name {{
- color: #1f2937;
- font-weight: 600;
- display: block;
- margin-bottom: 4px;
- }}
- .nav-match-hierarchy {{
- font-size: 11px;
- color: #9ca3af;
- display: flex;
- align-items: center;
- gap: 4px;
- flex-wrap: wrap;
- margin-bottom: 3px;
- }}
- .hierarchy-item {{
- display: inline-flex;
- align-items: center;
- gap: 4px;
- }}
- .hierarchy-separator {{
- color: #d1d5db;
- }}
- .nav-match-score {{
- color: #667eea;
- font-weight: 700;
- font-size: 13px;
- }}
- .inspirations-section {{
- flex: 1;
- height: 100vh;
- overflow-y: auto;
- background: #f5f7fa;
- position: relative;
- }}
- .breadcrumb-container {{
- position: sticky;
- top: 0;
- left: 0;
- right: 0;
- background: white;
- border-bottom: 1px solid #e5e7eb;
- padding: 15px 30px;
- z-index: 100;
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
- }}
- .breadcrumb {{
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- color: #6b7280;
- }}
- .breadcrumb-item {{
- display: flex;
- align-items: center;
- gap: 8px;
- }}
- .breadcrumb-separator {{
- color: #d1d5db;
- }}
- .breadcrumb-current {{
- color: #111827;
- font-weight: 600;
- }}
- .breadcrumb-label {{
- display: inline-block;
- font-size: 12px;
- color: #6366f1;
- background: #eef2ff;
- padding: 2px 8px;
- border-radius: 4px;
- font-weight: 500;
- margin-right: 4px;
- }}
- .inspiration-content-wrapper {{
- padding: 30px 40px;
- }}
- .inspirations-grid {{
- display: none;
- }}
- .inspiration-display {{
- width: 100%;
- }}
- .inspiration-detail {{
- display: none;
- }}
- .inspiration-detail.active {{
- display: block;
- }}
- .detail-header {{
- background: white;
- border-radius: 12px;
- padding: 25px 30px;
- margin-bottom: 15px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.06);
- border: 1px solid #e5e7eb;
- }}
- .detail-title-row {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- }}
- .detail-title-left, .detail-title-right {{
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .detail-label {{
- font-size: 12px;
- color: #5b21b6;
- background: rgba(109, 40, 217, 0.15);
- padding: 3px 10px;
- border-radius: 4px;
- font-weight: 500;
- }}
- .detail-category {{
- font-size: 18px;
- font-weight: 600;
- color: #374151;
- }}
- .detail-title {{
- font-size: 24px;
- font-weight: 700;
- color: #111827;
- margin-bottom: 12px;
- }}
- .detail-score-section {{
- display: flex;
- align-items: center;
- gap: 12px;
- }}
- .detail-score-label {{
- font-size: 13px;
- color: #6b7280;
- font-weight: 500;
- }}
- .detail-score-value {{
- font-size: 20px;
- font-weight: 700;
- color: #374151;
- }}
- .card-header {{
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 20px;
- gap: 12px;
- }}
- .inspiration-name {{
- font-size: 28px;
- font-weight: 800;
- 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;
- }}
- .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;
- }}
- .inspiration-category-section {{
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 25px;
- margin-bottom: 0;
- }}
- .info-detail-column {{
- background: #fafbfc;
- border-radius: 10px;
- border: 1px solid #e5e7eb;
- overflow: hidden;
- }}
- .info-header {{
- padding: 15px 20px;
- border-bottom: 1px solid #e5e7eb;
- background: #f9fafb;
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .info-label {{
- font-size: 11px;
- color: #8b5cf6;
- background: #f5f3ff;
- padding: 2px 6px;
- border-radius: 3px;
- font-weight: 500;
- }}
- .info-name {{
- font-size: 16px;
- font-weight: 600;
- color: #111827;
- }}
- .info-content {{
- padding: 20px;
- }}
- .info-field {{
- margin-bottom: 12px;
- line-height: 1.6;
- }}
- .info-field-label {{
- font-weight: 600;
- color: #374151;
- margin-right: 6px;
- }}
- .info-field-value {{
- color: #6b7280;
- }}
- .info-posts {{
- margin-top: 20px;
- }}
- .info-posts-title {{
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 12px;
- }}
- .info-posts-grid {{
- display: flex;
- gap: 15px;
- overflow-x: auto;
- overflow-y: hidden;
- padding-bottom: 10px;
- scroll-behavior: smooth;
- }}
- .info-posts-grid::-webkit-scrollbar {{
- height: 6px;
- }}
- .info-posts-grid::-webkit-scrollbar-track {{
- background: #f3f4f6;
- border-radius: 3px;
- }}
- .info-posts-grid::-webkit-scrollbar-thumb {{
- background: #d1d5db;
- border-radius: 3px;
- }}
- .info-posts-grid::-webkit-scrollbar-thumb:hover {{
- background: #9ca3af;
- }}
- .info-posts-grid .search-note-item {{
- flex: 0 0 260px;
- min-width: 260px;
- }}
- .matches-list {{
- display: flex;
- flex-direction: column;
- gap: 15px;
- }}
- .match-item {{
- background: white;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
- transition: all 0.3s;
- margin-bottom: 20px;
- }}
- .match-item:hover {{
- box-shadow: 0 4px 16px rgba(0,0,0,0.1);
- }}
- .match-main-header {{
- background: linear-gradient(135deg, #a5b4fc 0%, #c4b5fd 100%);
- border-bottom: none;
- padding: 16px 20px;
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }}
- .match-main-header:hover {{
- background: linear-gradient(135deg, #8b9cfc 0%, #b39dfc 100%);
- }}
- .match-header-row {{
- flex: 1;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 20px;
- }}
- .match-header-left, .match-header-right {{
- display: flex;
- align-items: center;
- gap: 10px;
- flex: 1;
- }}
- .match-header-center {{
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 4px;
- padding: 0 20px;
- }}
- .match-score-label {{
- font-size: 11px;
- color: #4c1d95;
- text-transform: uppercase;
- }}
- .match-score-value {{
- font-size: 20px;
- font-weight: 700;
- color: #5b21b6;
- }}
- .match-title {{
- color: #3730a3;
- font-size: 16px;
- font-weight: 700;
- }}
- .match-category {{
- color: #3730a3;
- font-size: 16px;
- font-weight: 700;
- }}
- .match-hierarchy {{
- color: #4c1d95;
- font-size: 13px;
- }}
- .match-toggle-main {{
- color: #5b21b6;
- font-size: 20px;
- transition: transform 0.3s;
- flex-shrink: 0;
- }}
- .match-item.expanded .match-toggle-main {{
- transform: rotate(180deg);
- }}
- .match-main-content {{
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease;
- }}
- .match-item.expanded .match-main-content {{
- max-height: 10000px;
- }}
- .match-main-content .inspiration-category-section {{
- margin: 25px;
- margin-bottom: 0;
- }}
- .step-section-wrapper {{
- margin: 20px 25px;
- border: 2px solid #e5e7eb;
- border-radius: 12px;
- overflow: hidden;
- background: white;
- }}
- .step-1-wrapper {{
- border-color: #fbbf24;
- }}
- .step-2-wrapper {{
- border-color: #34d399;
- }}
- .step-header {{
- background: linear-gradient(135deg, #fbbf24 0%, #fcd34d 100%);
- padding: 16px 25px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- cursor: pointer;
- transition: all 0.3s;
- }}
- .step-header:hover {{
- background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
- }}
- .step-2-wrapper .step-header {{
- background: linear-gradient(135deg, #34d399 0%, #6ee7b7 100%);
- }}
- .step-2-wrapper .step-header:hover {{
- background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
- }}
- .step-header-content {{
- display: flex;
- align-items: center;
- gap: 12px;
- }}
- .step-header .step-toggle {{
- font-size: 20px;
- color: #78350f;
- transition: transform 0.3s;
- }}
- .step-2-wrapper .step-header .step-toggle {{
- color: #065f46;
- }}
- .step-section-wrapper.expanded > .step-header .step-toggle {{
- transform: rotate(180deg);
- }}
- .step-wrapper-content {{
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease;
- }}
- .step-section-wrapper.expanded .step-wrapper-content {{
- max-height: 30000px;
- }}
- .step-number-badge {{
- background: rgba(255, 255, 255, 0.5);
- color: #78350f;
- padding: 6px 14px;
- border-radius: 20px;
- font-size: 13px;
- font-weight: 700;
- letter-spacing: 0.5px;
- }}
- .step-2-wrapper .step-number-badge {{
- color: #065f46;
- }}
- .step-title {{
- color: #78350f;
- font-size: 16px;
- font-weight: 700;
- }}
- .step-2-wrapper .step-title {{
- color: #065f46;
- }}
- .match-analysis-section {{
- padding: 25px;
- }}
- .match-header {{
- padding: 20px;
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 15px;
- background: #fafafa;
- transition: background 0.3s;
- border-bottom: 1px solid #e5e7eb;
- }}
- .match-header:hover {{
- background: #f5f5f5;
- }}
- .match-section.expanded .match-header {{
- background: rgba(102, 126, 234, 0.05);
- }}
- .match-header-left {{
- flex: 1;
- min-width: 0;
- }}
- .match-rank {{
- display: inline-block;
- padding: 3px 10px;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 600;
- background: rgba(91, 33, 182, 0.15);
- color: #5b21b6;
- }}
- .match-element-name {{
- font-size: 18px;
- font-weight: 700;
- color: #1f2937;
- display: block;
- margin-bottom: 6px;
- }}
- .match-context {{
- font-size: 13px;
- color: #6b7280;
- display: flex;
- align-items: center;
- gap: 6px;
- }}
- .match-context-separator {{
- color: #d1d5db;
- }}
- .match-header-right {{
- display: flex;
- align-items: center;
- gap: 15px;
- flex-shrink: 0;
- }}
- .match-score {{
- font-size: 24px;
- font-weight: 800;
- color: #667eea;
- }}
- .match-toggle {{
- width: 32px;
- height: 32px;
- border-radius: 8px;
- background: #e5e7eb;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s;
- font-size: 18px;
- color: #6b7280;
- }}
- .match-item.expanded .match-toggle {{
- background: #667eea;
- color: white;
- transform: rotate(180deg);
- }}
- .match-section {{
- border-bottom: 1px solid #e5e7eb;
- }}
- .match-section:last-child {{
- border-bottom: none;
- }}
- .step-section-header {{
- padding: 18px 25px;
- background: #f9fafb;
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #e5e7eb;
- }}
- .step-section-header:hover {{
- background: #f3f4f6;
- }}
- .step-section-title {{
- font-size: 15px;
- font-weight: 600;
- color: #374151;
- display: flex;
- align-items: center;
- gap: 10px;
- }}
- .step-sub-number {{
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 38px;
- padding: 4px 10px;
- background: #10b981;
- color: white;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 700;
- }}
- .step-number {{
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- background: #e5e7eb;
- color: #6b7280;
- border-radius: 50%;
- font-size: 11px;
- font-weight: 600;
- }}
- .step-toggle {{
- font-size: 18px;
- color: #6b7280;
- transition: transform 0.3s;
- }}
- .step-section.expanded .step-toggle {{
- transform: rotate(180deg);
- }}
- .step-section-content {{
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease;
- }}
- .step-section.expanded .step-section-content {{
- max-height: 20000px;
- }}
- .match-content {{
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease;
- }}
- .match-section.expanded .match-content {{
- max-height: 2000px;
- }}
- .match-content-inner {{
- padding: 20px;
- }}
- .match-explain {{
- background: #fffbeb;
- padding: 16px 20px;
- border-radius: 8px;
- margin-top: 20px;
- border-left: 3px solid #f59e0b;
- }}
- .match-explain-title {{
- font-weight: 600;
- color: #374151;
- margin-bottom: 6px;
- font-size: 13px;
- }}
- .match-explain-text {{
- color: #6b7280;
- font-size: 14px;
- line-height: 1.7;
- }}
- .match-parts-container {{
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 25px;
- margin-bottom: 20px;
- }}
- .match-parts-column {{
- min-width: 0;
- }}
- .match-parts {{
- margin-bottom: 0;
- height: 100%;
- background: #f9fafb;
- padding: 16px;
- border-radius: 8px;
- border: 1px solid #e5e7eb;
- }}
- .match-parts-title {{
- font-weight: 700;
- font-size: 14px;
- margin-bottom: 16px;
- color: #374151;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }}
- .part-item {{
- padding: 10px 0;
- margin-bottom: 8px;
- font-size: 14px;
- border-bottom: 1px solid #e5e7eb;
- }}
- .part-item:last-child {{
- border-bottom: none;
- }}
- .part-key {{
- font-weight: 500;
- color: #374151;
- margin-right: 8px;
- }}
- .part-value {{
- color: #6b7280;
- }}
- .same-parts {{
- background: #ecfdf5;
- border-color: #10b981;
- }}
- .increment-parts {{
- background: #fef3c7;
- border-color: #f59e0b;
- }}
- .search-results-section {{
- padding: 25px;
- }}
- .search-params-section {{
- background: #f0f9ff;
- border: 1px solid #bae6fd;
- border-radius: 8px;
- padding: 20px;
- margin-bottom: 20px;
- }}
- .search-params-title {{
- font-size: 15px;
- font-weight: 700;
- color: #0c4a6e;
- margin-bottom: 16px;
- }}
- .search-params-grid {{
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- }}
- .search-param-item {{
- display: flex;
- align-items: center;
- gap: 8px;
- }}
- .search-param-label {{
- font-size: 13px;
- color: #0369a1;
- font-weight: 600;
- min-width: 80px;
- }}
- .search-param-value {{
- font-size: 13px;
- color: #0c4a6e;
- background: #e0f2fe;
- padding: 4px 12px;
- border-radius: 4px;
- }}
- .search-summary-section {{
- background: #ecfdf5;
- border: 1px solid #a7f3d0;
- border-radius: 8px;
- padding: 16px 20px;
- margin-bottom: 20px;
- }}
- .search-summary-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 20px;
- }}
- .search-summary-left {{
- flex: 1;
- }}
- .search-summary-title {{
- font-size: 15px;
- font-weight: 700;
- color: #065f46;
- margin-bottom: 8px;
- }}
- .search-summary-content {{
- font-size: 14px;
- color: #047857;
- }}
- .search-result-count {{
- font-size: 18px;
- font-weight: 700;
- color: #059669;
- margin: 0 4px;
- }}
- .search-sort-buttons {{
- display: flex;
- align-items: center;
- gap: 8px;
- }}
- .search-sort-label {{
- font-size: 13px;
- color: #047857;
- font-weight: 600;
- }}
- .search-sort-btn {{
- background: white;
- border: 1px solid #a7f3d0;
- color: #065f46;
- padding: 6px 12px;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
- }}
- .search-sort-btn:hover {{
- background: #d1fae5;
- border-color: #6ee7b7;
- }}
- .search-sort-btn.active {{
- background: #059669;
- color: white;
- border-color: #059669;
- }}
- .note-position {{
- font-size: 11px;
- color: #9ca3af;
- font-weight: 600;
- }}
- .search-notes-list {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 16px;
- }}
- .search-note-item {{
- background: white;
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- border: 1px solid #e5e7eb;
- transition: all 0.3s;
- position: relative;
- }}
- .search-note-item:hover {{
- box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
- border-color: #667eea;
- }}
- .note-score-badge {{
- position: absolute;
- top: 12px;
- right: 12px;
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
- padding: 8px 14px;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 2px;
- cursor: pointer;
- z-index: 5;
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
- transition: all 0.3s;
- }}
- .note-score-badge:hover {{
- transform: scale(1.05);
- box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
- }}
- .note-score-badge .score-label {{
- font-size: 10px;
- color: rgba(255, 255, 255, 0.9);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }}
- .note-score-badge .score-value {{
- font-size: 18px;
- font-weight: 700;
- color: white;
- }}
- .note-image-carousel {{
- position: relative;
- width: 100%;
- min-height: 200px;
- max-height: 400px;
- background: #f3f4f6;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- }}
- .note-images-track {{
- display: flex;
- width: 100%;
- height: 100%;
- transition: transform 0.4s ease;
- }}
- .note-image {{
- flex: 0 0 100%;
- width: 100%;
- height: auto;
- max-height: 400px;
- object-fit: contain;
- }}
- .note-carousel-button {{
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- width: 32px;
- height: 32px;
- background: rgba(255, 255, 255, 0.9);
- border: none;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s;
- z-index: 10;
- font-size: 16px;
- color: #6b7280;
- opacity: 0;
- }}
- .search-note-item:hover .note-carousel-button {{
- opacity: 1;
- }}
- .note-carousel-button:hover {{
- background: #667eea;
- color: white;
- }}
- .note-carousel-button.disabled {{
- opacity: 0 !important;
- }}
- .note-carousel-button.prev {{
- left: 10px;
- }}
- .note-carousel-button.next {{
- right: 10px;
- }}
- .note-image-dots {{
- position: absolute;
- bottom: 10px;
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- gap: 6px;
- z-index: 10;
- }}
- .note-image-dot {{
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.6);
- transition: all 0.3s;
- }}
- .note-image-dot.active {{
- background: white;
- width: 20px;
- border-radius: 3px;
- }}
- .note-content {{
- padding: 15px;
- }}
- .note-title {{
- font-size: 15px;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 8px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- line-height: 1.4;
- }}
- .note-desc {{
- font-size: 13px;
- color: #6b7280;
- line-height: 1.6;
- margin-bottom: 12px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }}
- .note-publish-date {{
- font-size: 12px;
- color: #9ca3af;
- margin-bottom: 12px;
- display: flex;
- align-items: center;
- gap: 4px;
- }}
- .note-footer {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 12px;
- border-top: 1px solid #f3f4f6;
- }}
- .note-author {{
- font-size: 12px;
- font-weight: 600;
- color: #6b7280;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 150px;
- }}
- .note-points-hover {{
- position: absolute;
- bottom: 100%;
- left: 0;
- right: 0;
- background: white;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- padding: 12px;
- margin-bottom: 8px;
- opacity: 0;
- visibility: hidden;
- transition: opacity 0.2s, visibility 0.2s;
- z-index: 10;
- max-height: 300px;
- overflow-y: auto;
- }}
- .search-note-item {{
- position: relative;
- }}
- .search-note-item:hover .note-points-hover {{
- opacity: 1;
- visibility: visible;
- }}
- .note-points {{
- /* 用于详情弹窗中的样式 */
- }}
- .points-section {{
- margin-bottom: 8px;
- }}
- .points-section:last-child {{
- margin-bottom: 0;
- }}
- .points-label {{
- font-size: 11px;
- font-weight: 600;
- color: #6b7280;
- margin-bottom: 4px;
- }}
- .points-list {{
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- }}
- .point-item {{
- display: inline-block;
- }}
- .point-name {{
- display: inline-block;
- font-size: 11px;
- color: #4b5563;
- background: #f3f4f6;
- padding: 2px 6px;
- border-radius: 3px;
- }}
- .note-stats {{
- display: flex;
- gap: 10px;
- font-size: 12px;
- color: #9ca3af;
- }}
- .note-link {{
- display: block;
- text-align: center;
- padding: 8px;
- margin-top: 10px;
- background: rgba(102, 126, 234, 0.1);
- color: #667eea;
- text-decoration: none;
- border-radius: 6px;
- font-size: 13px;
- font-weight: 600;
- transition: all 0.3s;
- }}
- .note-link:hover {{
- background: #667eea;
- color: white;
- }}
- /* 笔记详情Modal样式 */
- .note-detail-modal {{
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.8);
- z-index: 9999;
- overflow-y: auto;
- padding: 40px 20px;
- }}
- .note-detail-modal.active {{
- display: flex;
- align-items: flex-start;
- justify-content: center;
- }}
- .note-detail-content {{
- background: white;
- border-radius: 16px;
- max-width: 1200px;
- width: 100%;
- position: relative;
- margin: auto;
- }}
- .note-detail-close {{
- position: absolute;
- top: 20px;
- right: 20px;
- width: 40px;
- height: 40px;
- background: white;
- border: none;
- border-radius: 50%;
- font-size: 24px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 2px 8px rgba(0,0,0,0.2);
- transition: all 0.3s;
- z-index: 10;
- }}
- .note-detail-close:hover {{
- background: #ef4444;
- color: white;
- transform: rotate(90deg);
- }}
- .note-detail-header {{
- padding: 30px;
- border-bottom: 1px solid #e5e7eb;
- }}
- .note-detail-title {{
- font-size: 24px;
- font-weight: 800;
- color: #1f2937;
- margin-bottom: 15px;
- }}
- .note-detail-meta {{
- display: flex;
- align-items: center;
- gap: 20px;
- font-size: 14px;
- color: #6b7280;
- }}
- .note-detail-author {{
- font-weight: 600;
- color: #1f2937;
- }}
- .note-detail-stats {{
- display: flex;
- gap: 15px;
- }}
- .note-detail-body {{
- padding: 30px;
- }}
- .note-detail-desc {{
- font-size: 15px;
- line-height: 1.8;
- color: #4b5563;
- margin-bottom: 30px;
- }}
- .note-detail-points {{
- margin-bottom: 30px;
- background: #f9fafb;
- border-radius: 8px;
- padding: 20px;
- }}
- .detail-points-section {{
- margin-bottom: 20px;
- }}
- .detail-points-section:last-child {{
- margin-bottom: 0;
- }}
- .detail-points-title {{
- font-size: 16px;
- font-weight: 600;
- color: #111827;
- margin-bottom: 12px;
- }}
- .detail-point-item {{
- background: white;
- padding: 12px 15px;
- border-radius: 6px;
- margin-bottom: 10px;
- border-left: 3px solid #e5e7eb;
- }}
- .detail-point-item:last-child {{
- margin-bottom: 0;
- }}
- .detail-point-name {{
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 4px;
- }}
- .detail-point-desc {{
- font-size: 13px;
- line-height: 1.6;
- color: #6b7280;
- }}
- .note-detail-images {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 15px;
- }}
- .note-detail-image {{
- width: 100%;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s;
- }}
- .note-detail-image:hover {{
- transform: scale(1.05);
- box-shadow: 0 8px 24px rgba(0,0,0,0.2);
- }}
- .note-detail-footer {{
- padding: 20px 30px;
- border-top: 1px solid #e5e7eb;
- display: flex;
- justify-content: center;
- }}
- .note-detail-link {{
- display: inline-block;
- padding: 12px 32px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- text-decoration: none;
- border-radius: 10px;
- font-weight: 600;
- transition: all 0.3s;
- }}
- .note-detail-link:hover {{
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
- }}
- /* 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样式 */
- .score-detail-content {{
- max-width: 800px;
- }}
- .score-detail-container {{
- padding: 10px;
- }}
- .score-detail-title {{
- font-size: 24px;
- font-weight: 700;
- color: #1f2937;
- margin-bottom: 20px;
- padding-bottom: 15px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .score-detail-score {{
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
- padding: 20px;
- border-radius: 12px;
- margin-bottom: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- }}
- .score-detail-label {{
- font-size: 16px;
- color: rgba(255, 255, 255, 0.9);
- font-weight: 600;
- }}
- .score-detail-value {{
- font-size: 32px;
- font-weight: 700;
- color: white;
- }}
- .score-detail-explain {{
- background: #fffbeb;
- padding: 16px 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border-left: 3px solid #f59e0b;
- }}
- .score-input-info {{
- background: #f0f9ff;
- padding: 16px 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border-left: 3px solid #3b82f6;
- }}
- .score-input-title {{
- font-weight: 700;
- font-size: 14px;
- margin-bottom: 12px;
- color: #1e40af;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }}
- .score-input-item {{
- margin-bottom: 12px;
- padding-bottom: 12px;
- border-bottom: 1px solid #bfdbfe;
- }}
- .score-input-item:last-child {{
- margin-bottom: 0;
- padding-bottom: 0;
- border-bottom: none;
- }}
- .score-input-label {{
- font-weight: 600;
- font-size: 13px;
- color: #1e40af;
- margin-bottom: 4px;
- }}
- .score-input-value {{
- font-size: 14px;
- color: #374151;
- line-height: 1.6;
- white-space: pre-wrap;
- word-break: break-word;
- }}
- .score-parts-container {{
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
- }}
- .score-parts {{
- background: #f9fafb;
- padding: 16px;
- border-radius: 8px;
- border: 1px solid #e5e7eb;
- }}
- .score-parts.same-parts {{
- background: #ecfdf5;
- border-color: #10b981;
- }}
- .score-parts.increment-parts {{
- background: #fef3c7;
- border-color: #f59e0b;
- }}
- .score-parts-title {{
- font-weight: 700;
- font-size: 14px;
- margin-bottom: 16px;
- color: #374151;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }}
- .score-part-item {{
- padding: 10px 0;
- margin-bottom: 8px;
- font-size: 14px;
- border-bottom: 1px solid #e5e7eb;
- }}
- .score-part-item:last-child {{
- border-bottom: none;
- }}
- .score-part-key {{
- font-weight: 500;
- color: #374151;
- margin-right: 8px;
- }}
- .score-part-value {{
- color: #6b7280;
- }}
- .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;
- }}
- @media (max-width: 768px) {{
- .main-content-layout {{
- flex-direction: column;
- }}
- .sidebar-nav {{
- position: relative;
- width: 100%;
- max-height: 300px;
- }}
- .inspirations-grid {{
- grid-template-columns: 1fr;
- }}
- .header h1 {{
- font-size: 32px;
- }}
- .stats-overview {{
- grid-template-columns: repeat(2, 1fr);
- }}
- }}
- </style>
- </head>
- <body>
- <div class="container">
- <div class="main-content-layout">
- <div class="sidebar-nav">
- <div class="nav-title">📑 灵感点列表</div>
- <div class="nav-list" id="navList">
- <!-- 目录将通过JS动态生成 -->
- </div>
- </div>
- <div class="inspirations-section">
- <div class="inspiration-display">
- {cards_html_str}
- </div>
- </div>
- </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>
- <!-- Score Detail Modal -->
- <div id="scoreDetailModal" class="modal-overlay" onclick="closeScoreDetailModal(event)">
- <div class="modal-content score-detail-content">
- <button class="modal-close" onclick="closeScoreDetail()">×</button>
- <div class="modal-body" id="scoreModalBody">
- <!-- Score detail 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
- import os
- # 配置路径(使用当前脚本的相对路径)
- script_dir = os.path.dirname(os.path.abspath(__file__))
- base_dir = os.path.join(script_dir, "data/阿里多多酱")
- inspiration_dir = os.path.join(base_dir, "out/人设_1110/how/灵感点")
- posts_dir = os.path.join(base_dir, "作者历史帖子")
- persona_path = os.path.join(base_dir, "out/人设_1110/人设.json")
- inspiration_to_post_path = os.path.join(base_dir, "out/人设_1110/点到帖子映射.json")
- category_index_path = os.path.join(base_dir, "out/人设_1110/分类索引_完整.json")
- post_to_mapping_path = os.path.join(base_dir, "out/人设_1110/帖子到分类和点映射.json")
- output_path = os.path.join(base_dir, "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"✅ 成功加载人设数据")
- print("\n📂 正在加载点到帖子映射数据...")
- with open(inspiration_to_post_path, 'r', encoding='utf-8') as f:
- inspiration_to_post_data = json.load(f)
- print(f"✅ 成功加载点到帖子映射数据")
- print("\n📂 正在加载分类索引数据...")
- with open(category_index_path, 'r', encoding='utf-8') as f:
- category_index_data = json.load(f)
- print(f"✅ 成功加载分类索引数据")
- print("\n📂 正在加载帖子到分类和点映射数据...")
- with open(post_to_mapping_path, 'r', encoding='utf-8') as f:
- post_to_mapping_data = json.load(f)
- print(f"✅ 成功加载帖子到分类和点映射数据")
- # 生成HTML
- print("\n🎨 正在生成可视化HTML...")
- result_path = generate_html(
- inspirations_data,
- posts_map,
- persona_data,
- output_path,
- inspiration_to_post_data,
- category_index_data,
- post_to_mapping_data
- )
- print(f"\n✅ 可视化文件已生成!")
- print(f"📄 文件路径: {result_path}")
- print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
- print("=" * 60)
- if __name__ == "__main__":
- main()
|