| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068 |
- """
- 灵感点分析结果可视化脚本
- 读取 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}")
- results.append({
- "summary": data,
- "step1": step1_data,
- "step2": step2_data,
- "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_inspiration_card_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成单个灵感点的卡片HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- HTML字符串
- """
- summary = inspiration_data.get("summary", {})
- step1 = inspiration_data.get("step1", {})
- step2 = inspiration_data.get("step2", {})
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- # 提取关键指标
- metrics = summary.get("关键指标", {})
- step1_score = metrics.get("step1_top1_score", 0)
- step2_score = metrics.get("step2_score", 0)
- step1_match_element = metrics.get("step1_top1_匹配要素", "")
- step2_increment_count = metrics.get("step2_增量词数量", 0)
- # 确定卡片颜色(基于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"
- # Step2颜色
- if step2_score >= 0.7:
- step2_color = "#10b981"
- elif step2_score >= 0.5:
- step2_color = "#f59e0b"
- elif step2_score >= 0.3:
- step2_color = "#3b82f6"
- else:
- step2_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("相同部分", {})
- increment_parts = match_result.get("增量部分", {})
- # 生成相同部分和增量部分的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>
- '''
- # 获取Step2匹配结果(简要展示)
- step2_match_preview = ""
- if step2:
- input_info = step2.get("输入信息", {})
- match_result = step2.get("匹配结果", {})
- increment_word = input_info.get("B", "")
- match_score = match_result.get("score", 0)
- same_parts = match_result.get("相同部分", {})
- increment_parts = match_result.get("增量部分", {})
- # 只有当增量词不为空时才显示
- if increment_word.strip():
- # 生成相同部分和增量部分的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>'
- step2_match_preview = f'''
- <div class="match-preview">
- <div class="match-preview-header">➕ Step2 Top1增量词</div>
- <div class="match-preview-content">
- <span class="match-preview-name">{html_module.escape(increment_word)}</span>
- <span class="match-preview-score" style="color: {step2_color};">{match_score:.2f}</span>
- </div>
- {parts_html}
- </div>
- '''
- # 准备详细数据用于弹窗
- detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
- detail_data_json_escaped = html_module.escape(detail_data_json)
- # 生成详细HTML并进行HTML转义
- detail_html = generate_detail_html(inspiration_data)
- detail_html_escaped = html_module.escape(detail_html)
- html = f'''
- <div class="inspiration-card" style="border-left-color: {border_color};"
- data-inspiration-name="{inspiration_name_escaped}"
- data-detail="{detail_data_json_escaped}"
- data-detail-html="{detail_html_escaped}"
- data-step1-score="{step1_score}"
- data-step2-score="{step2_score}"
- onclick="showInspirationDetail(this)">
- <div class="card-header">
- <h3 class="inspiration-name">{inspiration_name_escaped}</h3>
- </div>
- <div class="score-section">
- <div class="score-item">
- <div class="score-label">Step1分数</div>
- <div class="score-value" style="color: {step1_color};">{step1_score:.3f}</div>
- </div>
- <div class="score-divider"></div>
- <div class="score-item">
- <div class="score-label">Step2分数</div>
- <div class="score-value" style="color: {step2_color};">{step2_score:.3f}</div>
- </div>
- </div>
- {step1_match_preview}
- {step2_match_preview}
- <div class="metrics-section">
- <div class="metric-item">
- <span class="metric-icon">📊</span>
- <span class="metric-label">增量词数:</span>
- <span class="metric-value">{step2_increment_count}</span>
- </div>
- </div>
- <div class="click-hint">点击查看详情 →</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说明", "")
- same_parts = match_result.get("相同部分", {})
- increment_parts = match_result.get("增量部分", {})
- 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>
- '''
- # Step2 详细信息
- if step2 and step2.get("灵感"):
- input_info = step2.get("输入信息", {})
- match_result = step2.get("匹配结果", {})
- increment_word = input_info.get("B", "")
- b_context = input_info.get("B_Context", "")
- score = match_result.get("score", 0)
- score_explain = match_result.get("score说明", "")
- same_parts = match_result.get("相同部分", {})
- increment_parts = match_result.get("增量部分", {})
- content += '''
- <div class="modal-section">
- <h3>➕ Step2: 增量词匹配</h3>
- <div class="step-content">
- '''
- if increment_word.strip():
- content += f'''
- <div class="step-field">
- <span class="step-field-label">增量词:</span>
- <span class="step-field-value">{html_module.escape(increment_word)}</span>
- </div>
- '''
- if b_context:
- content += f'''
- <div class="increment-context">
- <strong>📌 增量词来源:</strong> {html_module.escape(b_context)}
- </div>
- '''
- content += f'''
- <div class="increment-item">
- <div class="increment-header">
- <span class="increment-words">分数</span>
- <span class="increment-score">{score:.2f}</span>
- </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>
- '''
- else:
- content += '''
- <div class="empty-state">暂无增量词匹配结果</div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 日志链接
- if metadata.get("log_url"):
- content += f'''
- <div class="modal-link">
- <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
- 🔗 查看详细日志
- </a>
- </div>
- '''
- return content
- def generate_detail_modal_content_js() -> str:
- """
- 生成详情弹窗内容的JavaScript函数
- Returns:
- JavaScript代码字符串
- """
- return '''
- // Tab切换功能
- function switchTab(event, tabId) {
- // 移除所有tab的active状态
- const tabButtons = document.querySelectorAll('.tab-button');
- tabButtons.forEach(button => {
- button.classList.remove('active');
- });
- // 隐藏所有tab内容
- const tabContents = document.querySelectorAll('.tab-content');
- tabContents.forEach(content => {
- content.classList.remove('active');
- });
- // 激活当前tab
- event.currentTarget.classList.add('active');
- document.getElementById(tabId).classList.add('active');
- }
- function showInspirationDetail(element) {
- const detailHtml = element.dataset.detailHtml;
- const modal = document.getElementById('detailModal');
- const modalBody = document.getElementById('modalBody');
- modalBody.innerHTML = detailHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- }
- function closeModal() {
- const modal = document.getElementById('detailModal');
- modal.classList.remove('active');
- document.body.style.overflow = '';
- }
- function closeModalOnOverlay(event) {
- if (event.target.id === 'detailModal') {
- closeModal();
- }
- }
- // ESC键关闭Modal
- document.addEventListener('keydown', function(event) {
- if (event.key === 'Escape') {
- closeModal();
- }
- });
- // 搜索和过滤功能
- function filterInspirations() {
- const searchInput = document.getElementById('searchInput').value.toLowerCase();
- const sortSelect = document.getElementById('sortSelect').value;
- const cards = document.querySelectorAll('.inspiration-card');
- let visibleCards = Array.from(cards);
- // 搜索过滤
- visibleCards.forEach(card => {
- const name = card.dataset.inspirationName.toLowerCase();
- if (name.includes(searchInput)) {
- card.style.display = '';
- } else {
- card.style.display = 'none';
- }
- });
- // 获取可见的卡片
- visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
- // 排序
- if (sortSelect === 'step1-desc' || sortSelect === 'step1-asc') {
- visibleCards.sort((a, b) => {
- const step1A = parseFloat(a.dataset.step1Score) || 0;
- const step1B = parseFloat(b.dataset.step1Score) || 0;
- const step2A = parseFloat(a.dataset.step2Score) || 0;
- const step2B = parseFloat(b.dataset.step2Score) || 0;
- if (sortSelect === 'step1-desc') {
- return step1B !== step1A ? step1B - step1A : step2B - step2A;
- } else {
- return step1A !== step1B ? step1A - step1B : step2A - step2B;
- }
- });
- } else if (sortSelect === 'step2-desc' || sortSelect === 'step2-asc') {
- visibleCards.sort((a, b) => {
- const step2A = parseFloat(a.dataset.step2Score) || 0;
- const step2B = parseFloat(b.dataset.step2Score) || 0;
- const step1A = parseFloat(a.dataset.step1Score) || 0;
- const step1B = parseFloat(b.dataset.step1Score) || 0;
- if (sortSelect === 'step2-desc') {
- return step2B !== step2A ? step2B - step2A : step1B - step1A;
- } else {
- return step2A !== step2B ? step2A - step2B : step1A - step1B;
- }
- });
- } else if (sortSelect === 'name-asc' || sortSelect === 'name-desc') {
- visibleCards.sort((a, b) => {
- const nameA = a.dataset.inspirationName;
- const nameB = b.dataset.inspirationName;
- return sortSelect === 'name-asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
- });
- }
- // 重新排列卡片
- const container = document.querySelector('.inspirations-grid');
- visibleCards.forEach(card => {
- container.appendChild(card);
- });
- // 更新统计
- updateStats();
- }
- function updateStats() {
- const cards = document.querySelectorAll('.inspiration-card');
- const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
- document.getElementById('totalCount').textContent = visibleCards.length;
- let step1ExcellentCount = 0;
- let step1GoodCount = 0;
- let step1NormalCount = 0;
- let step1NeedOptCount = 0;
- let step2ExcellentCount = 0;
- let step2GoodCount = 0;
- let step2NormalCount = 0;
- let step2NeedOptCount = 0;
- let totalStep1Score = 0;
- let totalStep2Score = 0;
- visibleCards.forEach(card => {
- const step1Score = parseFloat(card.dataset.step1Score) || 0;
- const step2Score = parseFloat(card.dataset.step2Score) || 0;
- totalStep1Score += step1Score;
- totalStep2Score += step2Score;
- // Step1 统计
- if (step1Score >= 0.7) step1ExcellentCount++;
- else if (step1Score >= 0.5) step1GoodCount++;
- else if (step1Score >= 0.3) step1NormalCount++;
- else step1NeedOptCount++;
- // Step2 统计
- if (step2Score >= 0.7) step2ExcellentCount++;
- else if (step2Score >= 0.5) step2GoodCount++;
- else if (step2Score >= 0.3) step2NormalCount++;
- else step2NeedOptCount++;
- });
- document.getElementById('step1ExcellentCount').textContent = step1ExcellentCount;
- document.getElementById('step1GoodCount').textContent = step1GoodCount;
- document.getElementById('step1NormalCount').textContent = step1NormalCount;
- document.getElementById('step1NeedOptCount').textContent = step1NeedOptCount;
- document.getElementById('step2ExcellentCount').textContent = step2ExcellentCount;
- document.getElementById('step2GoodCount').textContent = step2GoodCount;
- document.getElementById('step2NormalCount').textContent = step2NormalCount;
- document.getElementById('step2NeedOptCount').textContent = step2NeedOptCount;
- const avgStep1Score = visibleCards.length > 0 ? (totalStep1Score / visibleCards.length).toFixed(3) : '0.000';
- const avgStep2Score = visibleCards.length > 0 ? (totalStep2Score / visibleCards.length).toFixed(3) : '0.000';
- document.getElementById('avgStep1Score').textContent = avgStep1Score;
- document.getElementById('avgStep2Score').textContent = avgStep2Score;
- }
- '''
- def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
- """
- 生成人设结构的树状HTML
- Args:
- persona_data: 人设数据
- Returns:
- 人设结构的HTML字符串
- """
- if not persona_data:
- return '<div class="empty-state">暂无人设数据</div>'
- inspiration_list = persona_data.get("灵感点列表", [])
- if not inspiration_list:
- return '<div class="empty-state">暂无灵感点列表数据</div>'
- html_parts = ['<div class="tree">']
- for perspective_idx, perspective in enumerate(inspiration_list):
- perspective_name = perspective.get("视角名称", "未知视角")
- perspective_desc = perspective.get("视角描述", "")
- pattern_list = perspective.get("模式列表", [])
- # 一级节点:视角
- html_parts.append(f'''
- <ul>
- <li>
- <div class="tree-node level-1">
- <span class="node-icon">📁</span>
- <span class="node-name">{html_module.escape(perspective_name)}</span>
- <span class="node-count">{len(pattern_list)}个分类</span>
- </div>
- ''')
- if perspective_desc:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(perspective_desc)}</div>
- ''')
- # 二级节点:分类
- if pattern_list:
- html_parts.append('<ul>')
- for pattern in pattern_list:
- category_name = pattern.get("分类名称", "未知分类")
- core_definition = pattern.get("核心定义", "")
- subcategories = pattern.get("二级细分", [])
- total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
- html_parts.append(f'''
- <li>
- <div class="tree-node level-2">
- <span class="node-icon">📂</span>
- <span class="node-name">{html_module.escape(category_name)}</span>
- <span class="node-count">{total_posts}个帖子</span>
- </div>
- ''')
- if core_definition:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(core_definition)}</div>
- ''')
- # 三级节点:细分
- if subcategories:
- html_parts.append('<ul>')
- for subcategory in subcategories:
- sub_name = subcategory.get("分类名称", "未知细分")
- sub_definition = subcategory.get("分类定义", "")
- post_ids = subcategory.get("帖子ID列表", [])
- html_parts.append(f'''
- <li>
- <div class="tree-node level-3">
- <span class="node-icon">📄</span>
- <span class="node-name">{html_module.escape(sub_name)}</span>
- <span class="node-count">{len(post_ids)}个帖子</span>
- </div>
- ''')
- if sub_definition:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(sub_definition)}</div>
- ''')
- if post_ids:
- html_parts.append(f'''
- <div class="node-posts">
- <span class="posts-label">📋 帖子ID:</span>
- <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
- {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
- </div>
- ''')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</div>')
- return ''.join(html_parts)
- def generate_html(
- inspirations_data: List[Dict[str, Any]],
- posts_map: Dict[str, Dict[str, Any]],
- persona_data: Dict[str, Any],
- output_path: str
- ) -> str:
- """
- 生成完整的可视化HTML
- Args:
- inspirations_data: 灵感点数据列表
- posts_map: 帖子数据映射
- persona_data: 人设数据
- output_path: 输出文件路径
- Returns:
- 输出文件路径
- """
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- # 统计信息
- total_count = len(inspirations_data)
- # 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)
- # Step2 统计
- step2_excellent_count = sum(1 for d in inspirations_data
- if d["summary"].get("关键指标", {}).get("step2_score", 0) >= 0.7)
- step2_good_count = sum(1 for d in inspirations_data
- if 0.5 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.7)
- step2_normal_count = sum(1 for d in inspirations_data
- if 0.3 <= d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.5)
- step2_need_opt_count = sum(1 for d in inspirations_data
- if d["summary"].get("关键指标", {}).get("step2_score", 0) < 0.3)
- # 平均分数
- total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
- for d in inspirations_data)
- total_step2_score = sum(d["summary"].get("关键指标", {}).get("step2_score", 0)
- for d in inspirations_data)
- avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
- avg_step2_score = total_step2_score / total_count if total_count > 0 else 0
- # 按Step1分数排序(Step2作为次要排序)
- inspirations_data_sorted = sorted(
- inspirations_data,
- key=lambda x: (x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
- x["summary"].get("关键指标", {}).get("step2_score", 0)),
- reverse=True
- )
- # 生成卡片HTML
- cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted]
- cards_html_str = '\n'.join(cards_html)
- # 生成人设结构HTML
- persona_structure_html = generate_persona_structure_html(persona_data)
- # 生成JavaScript
- detail_modal_js = generate_detail_modal_content_js()
- # 完整HTML
- html_content = f'''<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>灵感点分析可视化</title>
- <style>
- * {{
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }}
- body {{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #333;
- line-height: 1.6;
- min-height: 100vh;
- padding: 20px;
- }}
- .container {{
- max-width: 1600px;
- margin: 0 auto;
- }}
- .header {{
- background: white;
- padding: 40px;
- border-radius: 16px;
- margin-bottom: 30px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
- }}
- .header h1 {{
- font-size: 42px;
- margin-bottom: 10px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: 800;
- }}
- .header-subtitle {{
- font-size: 16px;
- color: #6b7280;
- margin-bottom: 30px;
- }}
- .stats-overview {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: 20px;
- margin-top: 25px;
- }}
- .stat-box {{
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- padding: 20px;
- border-radius: 12px;
- text-align: center;
- transition: transform 0.3s ease;
- }}
- .stat-box:hover {{
- transform: translateY(-5px);
- }}
- .stat-label {{
- font-size: 13px;
- color: #6b7280;
- margin-bottom: 8px;
- font-weight: 600;
- }}
- .stat-value {{
- font-size: 32px;
- font-weight: 700;
- color: #1a1a1a;
- }}
- .stat-box.excellent .stat-value {{
- color: #10b981;
- }}
- .stat-box.good .stat-value {{
- color: #f59e0b;
- }}
- .stat-box.normal .stat-value {{
- color: #3b82f6;
- }}
- .stat-box.need-opt .stat-value {{
- color: #ef4444;
- }}
- .controls-section {{
- background: #f9fafb;
- padding: 25px;
- border-radius: 12px;
- margin-bottom: 30px;
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
- align-items: center;
- }}
- .search-box {{
- flex: 1;
- min-width: 250px;
- }}
- .search-input {{
- width: 100%;
- padding: 12px 20px;
- border: 2px solid #e5e7eb;
- border-radius: 10px;
- font-size: 15px;
- transition: all 0.3s;
- }}
- .search-input:focus {{
- outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
- }}
- .sort-box {{
- display: flex;
- align-items: center;
- gap: 12px;
- }}
- .sort-label {{
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- }}
- .sort-select {{
- padding: 10px 16px;
- border: 2px solid #e5e7eb;
- border-radius: 10px;
- font-size: 14px;
- background: white;
- cursor: pointer;
- transition: all 0.3s;
- }}
- .sort-select:focus {{
- outline: none;
- border-color: #667eea;
- }}
- .inspirations-section {{
- padding: 0;
- }}
- .inspirations-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
- gap: 25px;
- }}
- .inspiration-card {{
- background: white;
- border-radius: 14px;
- padding: 25px;
- border-left: 6px solid #10b981;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
- position: relative;
- }}
- .inspiration-card:hover {{
- transform: translateY(-8px);
- box-shadow: 0 12px 30px rgba(102, 126, 234, 0.2);
- }}
- .card-header {{
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 20px;
- gap: 12px;
- }}
- .inspiration-name {{
- font-size: 19px;
- font-weight: 700;
- color: #1a1a1a;
- line-height: 1.4;
- flex: 1;
- }}
- .grade-badge {{
- background: #10b981;
- color: white;
- padding: 6px 14px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 700;
- white-space: nowrap;
- }}
- .score-section {{
- display: flex;
- align-items: center;
- gap: 25px;
- margin-bottom: 20px;
- padding: 20px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- border-radius: 12px;
- }}
- .score-item {{
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- flex: 1;
- }}
- .main-score {{
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- }}
- .score-circle {{
- width: 90px;
- height: 90px;
- border-radius: 50%;
- border: 6px solid #10b981;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- }}
- .score-value {{
- font-size: 26px;
- font-weight: 800;
- color: #10b981;
- }}
- .score-label {{
- font-size: 12px;
- color: #6b7280;
- font-weight: 600;
- }}
- .sub-scores {{
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }}
- .sub-score-item {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 15px;
- background: white;
- border-radius: 8px;
- }}
- .sub-score-label {{
- font-size: 13px;
- color: #6b7280;
- font-weight: 600;
- }}
- .sub-score-value {{
- font-size: 18px;
- font-weight: 700;
- color: #2563eb;
- }}
- .metrics-section {{
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-bottom: 15px;
- }}
- .metric-item {{
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- color: #4b5563;
- }}
- .metric-icon {{
- font-size: 16px;
- }}
- .metric-label {{
- font-weight: 600;
- }}
- .metric-value {{
- color: #1f2937;
- }}
- .match-preview {{
- background: #f9fafb;
- padding: 12px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 3px solid #8b5cf6;
- }}
- .match-preview-header {{
- font-size: 12px;
- font-weight: 600;
- color: #6b7280;
- margin-bottom: 6px;
- }}
- .match-preview-content {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- }}
- .match-preview-name {{
- font-size: 13px;
- color: #1f2937;
- flex: 1;
- }}
- .match-preview-score {{
- font-size: 16px;
- font-weight: 700;
- }}
- .preview-parts {{
- margin-top: 8px;
- padding: 8px 10px;
- border-radius: 6px;
- font-size: 12px;
- line-height: 1.6;
- }}
- .preview-parts.same {{
- background: #f0fdf4;
- color: #15803d;
- border-left: 3px solid #10b981;
- }}
- .preview-parts.increment {{
- background: #fff7ed;
- color: #92400e;
- border-left: 3px solid #f59e0b;
- margin-top: 6px;
- }}
- .preview-parts strong {{
- font-weight: 700;
- margin-right: 6px;
- }}
- .score-divider {{
- width: 1px;
- height: 40px;
- background: #e5e7eb;
- }}
- .click-hint {{
- position: absolute;
- bottom: 15px;
- right: 15px;
- font-size: 12px;
- color: #8b5cf6;
- font-weight: 700;
- opacity: 0;
- transition: opacity 0.3s ease;
- background: rgba(139, 92, 246, 0.1);
- padding: 6px 12px;
- border-radius: 8px;
- }}
- .inspiration-card:hover .click-hint {{
- opacity: 1;
- }}
- /* Modal样式 */
- .modal-overlay {{
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- padding: 20px;
- overflow-y: auto;
- }}
- .modal-overlay.active {{
- display: flex;
- }}
- .modal-content {{
- background: white;
- border-radius: 16px;
- max-width: 1200px;
- width: 100%;
- max-height: 90vh;
- overflow-y: auto;
- position: relative;
- }}
- .modal-close {{
- position: sticky;
- top: 0;
- right: 0;
- background: white;
- border: none;
- font-size: 32px;
- color: #6b7280;
- cursor: pointer;
- padding: 15px 20px;
- z-index: 10;
- text-align: right;
- border-bottom: 1px solid #e5e7eb;
- }}
- .modal-close:hover {{
- color: #1f2937;
- }}
- .modal-body {{
- padding: 30px;
- }}
- .modal-header {{
- margin-bottom: 25px;
- padding-bottom: 20px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .modal-title {{
- font-size: 28px;
- font-weight: 800;
- color: #1a1a1a;
- }}
- .modal-section {{
- margin-bottom: 30px;
- }}
- .modal-section h3 {{
- font-size: 20px;
- font-weight: 700;
- color: #374151;
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 2px solid #f3f4f6;
- }}
- .info-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 15px;
- }}
- .info-item {{
- background: #f9fafb;
- padding: 12px 16px;
- border-radius: 8px;
- border-left: 3px solid #8b5cf6;
- }}
- .info-label {{
- font-weight: 600;
- color: #6b7280;
- font-size: 13px;
- margin-right: 8px;
- }}
- .info-value {{
- color: #1f2937;
- font-size: 14px;
- }}
- .metrics-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 15px;
- }}
- .metric-box {{
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- padding: 20px;
- border-radius: 12px;
- text-align: center;
- color: white;
- }}
- .metric-box.wide {{
- grid-column: span 2;
- }}
- .metric-box-label {{
- font-size: 13px;
- opacity: 0.9;
- margin-bottom: 8px;
- font-weight: 600;
- }}
- .metric-box-value {{
- font-size: 28px;
- font-weight: 700;
- }}
- .metric-box-value.small {{
- font-size: 16px;
- }}
- .step-content {{
- background: #f9fafb;
- padding: 20px;
- border-radius: 12px;
- }}
- .step-field {{
- margin-bottom: 20px;
- }}
- .step-field-label {{
- font-weight: 700;
- color: #374151;
- font-size: 14px;
- margin-bottom: 8px;
- display: block;
- }}
- .step-field-value {{
- color: #1f2937;
- font-size: 15px;
- line-height: 1.7;
- }}
- .matches-list {{
- display: flex;
- flex-direction: column;
- gap: 15px;
- margin-top: 10px;
- }}
- .match-item {{
- background: white;
- padding: 18px;
- border-radius: 10px;
- border-left: 5px solid #3b82f6;
- }}
- .match-item.top1 {{
- border-left-color: #fbbf24;
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
- }}
- .match-item.top2 {{
- border-left-color: #c0c0c0;
- background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
- }}
- .match-item.top3 {{
- border-left-color: #cd7f32;
- background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
- }}
- .match-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- gap: 10px;
- }}
- .match-rank {{
- font-size: 18px;
- font-weight: 800;
- color: #6b7280;
- }}
- .match-element-name {{
- flex: 1;
- font-size: 16px;
- font-weight: 700;
- color: #1f2937;
- }}
- .match-score {{
- font-size: 22px;
- font-weight: 800;
- color: #2563eb;
- background: white;
- padding: 6px 14px;
- border-radius: 8px;
- }}
- .match-detail {{
- background: rgba(255, 255, 255, 0.7);
- padding: 10px;
- border-radius: 6px;
- margin-bottom: 10px;
- font-size: 13px;
- color: #4b5563;
- }}
- .match-reason {{
- color: #1f2937;
- font-size: 14px;
- line-height: 1.7;
- }}
- .increment-matches {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 10px;
- }}
- .increment-item {{
- background: white;
- padding: 15px;
- border-radius: 8px;
- border-left: 4px solid #10b981;
- }}
- .increment-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }}
- .increment-words {{
- font-weight: 700;
- color: #1f2937;
- font-size: 15px;
- }}
- .increment-score {{
- font-size: 20px;
- font-weight: 800;
- color: #10b981;
- }}
- .increment-reason {{
- color: #4b5563;
- font-size: 13px;
- line-height: 1.6;
- }}
- .empty-state {{
- text-align: center;
- padding: 40px;
- color: #9ca3af;
- font-size: 14px;
- }}
- .modal-link {{
- margin-top: 25px;
- padding-top: 20px;
- border-top: 2px solid #e5e7eb;
- text-align: center;
- }}
- .modal-link-btn {{
- display: inline-flex;
- align-items: center;
- gap: 10px;
- padding: 12px 24px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- text-decoration: none;
- border-radius: 10px;
- font-size: 15px;
- font-weight: 600;
- transition: all 0.3s;
- }}
- .modal-link-btn:hover {{
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
- }}
- .timestamp {{
- text-align: center;
- color: white;
- font-size: 13px;
- margin-top: 30px;
- opacity: 0.8;
- }}
- .match-context {{
- background: #f3f4f6;
- padding: 8px 12px;
- border-radius: 6px;
- margin: 8px 0;
- font-size: 12px;
- color: #6b7280;
- line-height: 1.6;
- }}
- .match-explain {{
- background: #fef3c7;
- padding: 10px 12px;
- border-radius: 6px;
- margin: 10px 0;
- font-size: 13px;
- color: #92400e;
- line-height: 1.7;
- border-left: 3px solid #f59e0b;
- }}
- .match-parts {{
- margin: 12px 0;
- border-radius: 8px;
- overflow: hidden;
- }}
- .match-parts.same-parts {{
- background: #f0fdf4;
- border: 2px solid #10b981;
- }}
- .match-parts.increment-parts {{
- background: #fff7ed;
- border: 2px solid #f59e0b;
- }}
- .parts-header {{
- font-weight: 700;
- padding: 10px 12px;
- font-size: 13px;
- }}
- .same-parts .parts-header {{
- background: #dcfce7;
- color: #15803d;
- }}
- .increment-parts .parts-header {{
- background: #fed7aa;
- color: #92400e;
- }}
- .parts-content {{
- padding: 8px 12px;
- }}
- .part-item {{
- padding: 6px 0;
- border-bottom: 1px solid rgba(0,0,0,0.05);
- font-size: 13px;
- line-height: 1.6;
- }}
- .part-item:last-child {{
- border-bottom: none;
- }}
- .part-key {{
- font-weight: 600;
- color: #374151;
- margin-right: 6px;
- }}
- .part-value {{
- color: #1f2937;
- }}
- .increment-context {{
- background: #fef3c7;
- padding: 10px 12px;
- border-radius: 6px;
- margin: 10px 0;
- font-size: 12px;
- color: #92400e;
- line-height: 1.6;
- border-left: 3px solid #f59e0b;
- }}
- /* Tab样式 */
- .tabs-nav {{
- background: white;
- padding: 0 30px;
- border-radius: 16px 16px 0 0;
- margin-bottom: 0;
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
- display: flex;
- gap: 10px;
- }}
- .tab-button {{
- padding: 15px 30px;
- border: none;
- background: transparent;
- color: #6b7280;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- border-bottom: 3px solid transparent;
- transition: all 0.3s;
- }}
- .tab-button:hover {{
- color: #667eea;
- background: rgba(102, 126, 234, 0.05);
- }}
- .tab-button.active {{
- color: #667eea;
- border-bottom-color: #667eea;
- background: rgba(102, 126, 234, 0.05);
- }}
- .tab-content {{
- display: none;
- background: white;
- padding: 30px;
- border-radius: 0 0 16px 16px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.15);
- }}
- .tab-content.active {{
- display: block;
- }}
- /* 人设结构样式 */
- .persona-structure-section h2 {{
- font-size: 28px;
- font-weight: 700;
- margin-bottom: 25px;
- color: #1a1a1a;
- }}
- /* 树状图样式 */
- .tree {{
- font-size: 14px;
- }}
- .tree ul {{
- padding-left: 30px;
- list-style: none;
- position: relative;
- }}
- .tree ul ul {{
- padding-left: 40px;
- }}
- .tree li {{
- position: relative;
- padding: 8px 0;
- }}
- .tree li::before {{
- content: "";
- position: absolute;
- top: 0;
- left: -20px;
- border-left: 2px solid #d1d5db;
- border-bottom: 2px solid #d1d5db;
- width: 20px;
- height: 20px;
- }}
- .tree li::after {{
- content: "";
- position: absolute;
- top: 20px;
- left: -20px;
- border-left: 2px solid #d1d5db;
- height: 100%;
- }}
- .tree li:last-child::after {{
- display: none;
- }}
- .tree > ul > li::before,
- .tree > ul > li::after {{
- display: none;
- }}
- .tree-node {{
- display: inline-flex;
- align-items: center;
- gap: 10px;
- padding: 12px 16px;
- border-radius: 8px;
- transition: all 0.3s;
- margin-bottom: 8px;
- }}
- .tree-node:hover {{
- transform: translateX(4px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
- }}
- .tree-node.level-1 {{
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-size: 18px;
- font-weight: 700;
- padding: 16px 20px;
- }}
- .tree-node.level-2 {{
- background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
- color: #1e40af;
- font-size: 16px;
- font-weight: 600;
- border: 2px solid #3b82f6;
- }}
- .tree-node.level-3 {{
- background: white;
- color: #374151;
- font-size: 14px;
- font-weight: 500;
- border: 1px solid #e5e7eb;
- }}
- .node-icon {{
- font-size: 20px;
- }}
- .tree-node.level-1 .node-icon {{
- font-size: 24px;
- }}
- .node-name {{
- flex: 1;
- }}
- .node-count {{
- background: rgba(255, 255, 255, 0.3);
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }}
- .tree-node.level-1 .node-count {{
- background: rgba(255, 255, 255, 0.4);
- }}
- .tree-node.level-2 .node-count {{
- background: #bfdbfe;
- color: #1e3a8a;
- }}
- .tree-node.level-3 .node-count {{
- background: #dcfce7;
- color: #166534;
- }}
- .node-desc {{
- margin: 8px 0 8px 50px;
- padding: 12px 16px;
- background: #fffbeb;
- border-left: 3px solid #f59e0b;
- border-radius: 6px;
- color: #92400e;
- font-size: 13px;
- line-height: 1.7;
- }}
- .node-posts {{
- margin: 8px 0 8px 50px;
- padding: 12px 16px;
- background: #f0fdf4;
- border-left: 3px solid #10b981;
- border-radius: 6px;
- font-size: 12px;
- line-height: 1.8;
- }}
- .posts-label {{
- font-weight: 600;
- color: #15803d;
- margin-right: 8px;
- }}
- .posts-ids {{
- color: #166534;
- word-break: break-all;
- }}
- .posts-more {{
- color: #059669;
- font-weight: 600;
- margin-left: 8px;
- }}
- @media (max-width: 768px) {{
- .inspirations-grid {{
- grid-template-columns: 1fr;
- }}
- .header h1 {{
- font-size: 32px;
- }}
- .stats-overview {{
- grid-template-columns: repeat(2, 1fr);
- }}
- }}
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💡 灵感点分析可视化</h1>
- <div class="header-subtitle">基于HOW人设的灵感点匹配分析结果</div>
- <div class="stats-overview">
- <div class="stat-box">
- <div class="stat-label">分析总数</div>
- <div class="stat-value" id="totalCount">{total_count}</div>
- </div>
- <div class="stat-box excellent">
- <div class="stat-label">Step1优秀 (≥0.7)</div>
- <div class="stat-value" id="step1ExcellentCount">{step1_excellent_count}</div>
- </div>
- <div class="stat-box good">
- <div class="stat-label">Step1良好 (0.5-0.7)</div>
- <div class="stat-value" id="step1GoodCount">{step1_good_count}</div>
- </div>
- <div class="stat-box normal">
- <div class="stat-label">Step1一般 (0.3-0.5)</div>
- <div class="stat-value" id="step1NormalCount">{step1_normal_count}</div>
- </div>
- <div class="stat-box need-opt">
- <div class="stat-label">Step1待优化 (<0.3)</div>
- <div class="stat-value" id="step1NeedOptCount">{step1_need_opt_count}</div>
- </div>
- <div class="stat-box excellent">
- <div class="stat-label">Step2优秀 (≥0.7)</div>
- <div class="stat-value" id="step2ExcellentCount">{step2_excellent_count}</div>
- </div>
- <div class="stat-box good">
- <div class="stat-label">Step2良好 (0.5-0.7)</div>
- <div class="stat-value" id="step2GoodCount">{step2_good_count}</div>
- </div>
- <div class="stat-box normal">
- <div class="stat-label">Step2一般 (0.3-0.5)</div>
- <div class="stat-value" id="step2NormalCount">{step2_normal_count}</div>
- </div>
- <div class="stat-box need-opt">
- <div class="stat-label">Step2待优化 (<0.3)</div>
- <div class="stat-value" id="step2NeedOptCount">{step2_need_opt_count}</div>
- </div>
- <div class="stat-box">
- <div class="stat-label">Step1平均分</div>
- <div class="stat-value" id="avgStep1Score">{avg_step1_score:.3f}</div>
- </div>
- <div class="stat-box">
- <div class="stat-label">Step2平均分</div>
- <div class="stat-value" id="avgStep2Score">{avg_step2_score:.3f}</div>
- </div>
- </div>
- </div>
- <div class="tabs-nav">
- <button class="tab-button active" onclick="switchTab(event, 'tab-inspirations')">
- 灵感点分析
- </button>
- <button class="tab-button" onclick="switchTab(event, 'tab-persona')">
- 人设结构
- </button>
- </div>
- <div id="tab-inspirations" class="tab-content active">
- <div class="controls-section">
- <div class="search-box">
- <input type="text"
- id="searchInput"
- class="search-input"
- placeholder="🔍 搜索灵感点名称..."
- oninput="filterInspirations()">
- </div>
- <div class="sort-box">
- <span class="sort-label">排序方式:</span>
- <select id="sortSelect" class="sort-select" onchange="filterInspirations()">
- <option value="step1-desc">Step1分数从高到低</option>
- <option value="step1-asc">Step1分数从低到高</option>
- <option value="step2-desc">Step2分数从高到低</option>
- <option value="step2-asc">Step2分数从低到高</option>
- <option value="name-asc">名称A-Z</option>
- <option value="name-desc">名称Z-A</option>
- </select>
- </div>
- </div>
- <div class="inspirations-section">
- <div class="inspirations-grid">
- {cards_html_str}
- </div>
- </div>
- </div>
- <div id="tab-persona" class="tab-content">
- <div class="persona-structure-section">
- <h2>📚 人设结构</h2>
- {persona_structure_html}
- </div>
- </div>
- <div class="timestamp">生成时间: {timestamp}</div>
- <!-- Modal -->
- <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
- <div class="modal-content">
- <button class="modal-close" onclick="closeModal()">×</button>
- <div class="modal-body" id="modalBody">
- <!-- Content will be inserted here -->
- </div>
- </div>
- </div>
- </div>
- <script>
- {detail_modal_js}
- </script>
- </body>
- </html>'''
- # 写入文件
- output_file = Path(output_path)
- output_file.parent.mkdir(parents=True, exist_ok=True)
- with open(output_file, 'w', encoding='utf-8') as f:
- f.write(html_content)
- return str(output_file.absolute())
- def load_persona_data(persona_path: str) -> Dict[str, Any]:
- """
- 加载人设数据
- Args:
- persona_path: 人设JSON文件路径
- Returns:
- 人设数据字典
- """
- try:
- with open(persona_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except Exception as e:
- print(f"警告: 读取人设文件失败: {e}")
- return {}
- def main():
- """主函数"""
- import sys
- # 配置路径
- inspiration_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点"
- posts_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/作者历史帖子"
- persona_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/人设.json"
- output_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点可视化.html"
- print("=" * 60)
- print("灵感点分析可视化脚本")
- print("=" * 60)
- # 加载数据
- print("\n📂 正在加载灵感点数据...")
- inspirations_data = load_inspiration_points_data(inspiration_dir)
- print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
- print("\n📂 正在加载帖子数据...")
- posts_map = load_posts_data(posts_dir)
- print(f"✅ 成功加载 {len(posts_map)} 个帖子")
- print("\n📂 正在加载人设数据...")
- persona_data = load_persona_data(persona_path)
- print(f"✅ 成功加载人设数据")
- # 生成HTML
- print("\n🎨 正在生成可视化HTML...")
- result_path = generate_html(inspirations_data, posts_map, persona_data, output_path)
- print(f"\n✅ 可视化文件已生成!")
- print(f"📄 文件路径: {result_path}")
- print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
- print("=" * 60)
- if __name__ == "__main__":
- main()
|