# src — HTML 文件汇总

> **生成时间**：2026-08-05 02:34:35 UTC  
> **根目录**：`C:\Users\s84336076\IDEProjects\recruiter-navi\src`

## 路径：`recruiter_navigator\web\templates\base.html`

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Recruiter Tool Navigator{% endblock %}</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <link rel="stylesheet" href="/static/css/base.css">
    {% block head %}{% endblock %}
</head>
<body class="bg-gray-50 min-h-screen">
    {% block content %}{% endblock %}
</body>
</html>

```

---

## 路径：`recruiter_navigator\web\templates\home.html`

```html
{% extends "base.html" %}

{% block title %}招聘流程信息图 — Recruiter Tool Navigator{% endblock %}

{% block head %}
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/home.css">
{% endblock %}

{% block content %}
<div x-data="homeInfographic()" class="min-h-screen bg-slate-50">
    <!-- 顶部导航 -->
    <header class="bg-white/80 backdrop-blur-xl border-b border-slate-200/80 sticky top-0 z-40">
        <div class="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
            <div class="flex items-center gap-4">
                <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/20">
                    <span class="text-white text-lg font-bold">R</span>
                </div>
                <div>
                    <h1 class="text-lg font-bold text-slate-900 tracking-tight">招聘流程信息图</h1>
                    <p class="text-xs text-slate-400 font-medium">47 tools · 5 阶段 · 可视化工作流</p>
                </div>
            </div>
            <div class="flex items-center gap-3">
                <a href="/" class="px-4 py-2 bg-slate-100 text-slate-600 rounded-xl text-sm font-medium hover:bg-slate-200 transition-all">工具列表</a>
                <div class="relative">
                    <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
                    </svg>
                    <input
                        type="text"
                        x-model="searchQuery"
                        placeholder="搜索工具..."
                        class="w-56 pl-10 pr-4 py-2 bg-slate-100 border-0 rounded-xl text-sm text-slate-700 placeholder-slate-400 focus:ring-2 focus:ring-blue-500/30 focus:bg-white transition-all"
                    />
                </div>
            </div>
        </div>
    </header>

    <!-- 场景快捷入口 - 词云展示 -->
    <div class="bg-white border-b border-slate-200/80">
        <div class="max-w-7xl mx-auto px-6 py-5">
            <div class="flex items-center justify-between mb-3">
                <div class="flex items-center gap-2">
                    <span class="text-sm font-semibold text-slate-700">典型招聘场景</span>
                    <span class="text-xs text-slate-400">点击词云查看完整工具链路</span>
                </div>
                <div class="flex items-center gap-2">
                    <button 
                        @click="toggleWordCloudAnimation()"
                        :class="wordCloudAnimation 
                            ? 'bg-blue-100 text-blue-700 border-blue-200' 
                            : 'bg-slate-100 text-slate-600 border-slate-200'"
                        class="px-3 py-1.5 rounded-lg text-xs font-medium border transition-all flex items-center gap-1.5"
                    >
                        <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path x-show="wordCloudAnimation" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
                            <path x-show="!wordCloudAnimation" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
                        </svg>
                        <span x-text="wordCloudAnimation ? '暂停轮播' : '开始轮播'"></span>
                    </button>
                </div>
            </div>
            
            <!-- 词云展示区域 -->
            <div class="word-cloud-container">
                <!-- 词云标签 -->
                <div class="word-cloud-tag" x-show="activeScenario">
                    <span x-text="activeScenarioData?.title || '选择场景'"></span>
                </div>
                
                <!-- 词云信息面板 -->
                <div class="word-cloud-info" x-show="wordCloudWords.length > 0">
                    <div class="word-cloud-info-title">工具链路</div>
                    <div class="word-cloud-info-desc" x-text="activeScenarioData?.desc || '点击词云中的工具查看详情'"></div>
                </div>
                
                <!-- 词云文字 -->
                <div class="relative w-full h-full">
                    <template x-for="(word, index) in wordCloudWords" :key="index">
                        <div 
                            class="word-cloud-word"
                            :class="word.active ? 'active animated' : ''"
                            :style="getWordCloudStyle(word)"
                            @click="selectWordCloudWord(index)"
                            x-text="word.text"
                            :title="word.text"
                        ></div>
                    </template>
                    
                    <!-- 默认提示 -->
                    <div x-show="wordCloudWords.length === 0" class="absolute inset-0 flex items-center justify-center">
                        <div class="text-center">
                            <div class="text-4xl mb-2 opacity-20">☁️</div>
                            <p class="text-sm text-slate-400 font-medium">点击下方场景生成词云</p>
                            <p class="text-xs text-slate-300 mt-1">动态展示工具链路关系</p>
                        </div>
                    </div>
                </div>
                
                <!-- 词云控制面板 -->
                <div class="word-cloud-controls" x-show="wordCloudWords.length > 0">
                    <button 
                        @click="wordCloudCarouselSpeed = Math.max(1000, wordCloudCarouselSpeed - 500)"
                        class="word-cloud-btn"
                        title="加快轮播"
                    >
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z" />
                        </svg>
                    </button>
                    <button 
                        @click="toggleWordCloudAnimation()"
                        :class="wordCloudAnimation ? 'active' : ''"
                        class="word-cloud-btn"
                        :title="wordCloudAnimation ? '暂停轮播' : '开始轮播'"
                    >
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path x-show="wordCloudAnimation" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
                            <path x-show="!wordCloudAnimation" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
                        </svg>
                    </button>
                    <button 
                        @click="wordCloudCarouselSpeed = Math.min(5000, wordCloudCarouselSpeed + 500)"
                        class="word-cloud-btn"
                        title="减慢轮播"
                    >
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 15l-3-3m0 0l3-3m-3 3h8M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                        </svg>
                    </button>
                </div>
                
                <!-- 词云轮播指示器 -->
                <div class="word-cloud-indicators" x-show="wordCloudWords.length > 0">
                    <template x-for="(word, index) in wordCloudWords" :key="index">
                        <div 
                            class="word-cloud-indicator"
                            :class="word.active ? 'active' : ''"
                            @click="selectWordCloudWord(index)"
                            :title="word.text"
                        ></div>
                    </template>
                </div>
            </div>
            
            <!-- 场景选择按钮 -->
            <div class="flex gap-2 overflow-x-auto pb-1 scrollbar-none mt-4">
                <template x-for="s in scenarios" :key="s.id">
                    <button
                        @click="toggleScenario(s.id)"
                        :class="activeScenario === s.id
                            ? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg shadow-blue-500/20'
                            : 'bg-gradient-to-r from-slate-100 to-slate-50 text-slate-600 hover:from-slate-200 hover:to-slate-100'"
                        class="px-4 py-2 rounded-xl text-sm font-medium transition-all whitespace-nowrap shrink-0 border border-slate-200/50 hover:border-slate-300"
                    >
                        <span x-text="s.title"></span>
                    </button>
                </template>
            </div>
            
            <!-- 场景链路展示 -->
            <div x-show="activeScenario" x-cloak
                x-transition:enter="transition ease-out duration-200"
                x-transition:enter-start="opacity-0 -translate-y-1"
                x-transition:enter-end="opacity-100 translate-y-0"
                x-transition:leave="transition ease-in duration-150"
                x-transition:leave-start="opacity-100 translate-y-0"
                x-transition:leave-end="opacity-0 -translate-y-1"
                class="mt-4 bg-gradient-to-br from-blue-50/50 to-indigo-50/50 rounded-2xl p-5 border border-blue-100/50"
            >
                <template x-if="activeScenarioData">
                    <div>
                        <p class="text-sm text-slate-500 mb-3" x-text="activeScenarioData.desc"></p>
                        <div class="flex flex-wrap items-center gap-2">
                            <template x-for="(toolName, i) in activeScenarioData.chain" :key="toolName + i">
                                <div class="scenario-chain-item">
                                    <span
                                        class="px-3 py-1.5 bg-white text-slate-700 rounded-lg text-sm font-medium cursor-pointer hover:bg-blue-50 hover:text-blue-700 transition-colors border border-slate-200 hover:border-blue-200"
                                        @click="highlightToolChain(toolName)"
                                        x-text="getToolDisplayName(toolName)"
                                    ></span>
                                    <template x-if="i < activeScenarioData.chain.length - 1">
                                        <span class="arrow">→</span>
                                    </template>
                                </div>
                            </template>
                        </div>
                    </div>
                </template>
            </div>
        </div>
    </div>

    <!-- 主体：时间线 + 阶段内容 -->
    <div class="max-w-7xl mx-auto px-6 py-10">
        <div class="relative flex gap-10">
            <!-- 左侧时间线（桌面端） -->
            <div class="timeline timeline--desktop hidden md:block shrink-0"
                 role="navigation" 
                 aria-label="招聘流程时间线导航"
                 data-testid="timeline-navigation">
                <div class="timeline-track" role="presentation" aria-hidden="true"></div>
                <div class="timeline__items sticky top-28" role="list" aria-label="招聘流程阶段">
                    <template x-for="(stage, index) in stages" :key="stage.id">
                        <div class="timeline__item"
                             role="listitem"
                             :data-stage-id="stage.id"
                             :data-stage-index="index"
                             :aria-current="activeStage === stage.id ? 'true' : 'false'"
                             :tabindex="activeStage === stage.id ? '0' : '-1'"
                             @click="scrollToStage(stage.id)"
                             @keydown="handleTimelineKeydown($event, stage.id)">
                            <!-- 文字标签（左侧） -->
                            <div class="timeline__label-container">
                                <span class="timeline__label text-[11px] font-semibold leading-tight transition-colors block"
                                      :class="{'timeline__label--active text-slate-800': activeStage === stage.id, 
                                               'timeline__label--inactive text-slate-400 group-hover:text-slate-600': activeStage !== stage.id}"
                                      :aria-label="'跳转到' + stage.label + '阶段'"
                                      x-text="stage.label">
                                </span>
                            </div>
                            
                            <!-- 时间点（右侧） -->
                            <div class="timeline__dot-container">
                                <button class="timeline-dot timeline__dot"
                                        :class="{'active timeline__dot--active': activeStage === stage.id}"
                                        :style="'background-color:' + (stage.color || '#94a3b8'); '--dot-color: ' + (stage.color || '#94a3b8')"
                                        :aria-label="stage.label + '阶段，' + getStageToolCount(stage.id) + '个工具'"
                                        :aria-pressed="activeStage === stage.id ? 'true' : 'false'"
                                        @click.stop="scrollToStage(stage.id)">
                                    <span class="timeline__dot-icon" 
                                          :aria-hidden="true"
                                          x-text="stage.icon || '●'">
                                    </span>
                                    <span class="timeline__dot-tooltip sr-only" 
                                          role="tooltip"
                                          x-text="stage.label">
                                    </span>
                                </button>
                            </div>
                        </div>
                    </template>
                </div>
            </div>

            <!-- 右侧阶段内容 -->
            <div class="flex-1 space-y-12">
                <template x-for="stage in stages" :key="stage.id">
                    <div :id="'stage-' + stage.id" class="scroll-mt-28">
                        <!-- 阶段标题 -->
                        <div class="stage-header mb-5" :style="'--stage-color:' + stage.color">
                            <div class="stage-header-line" :style="'background-color:' + stage.color"></div>
                            <div class="flex items-center gap-3">
                                <div
                                    class="w-9 h-9 rounded-xl flex items-center justify-center text-base md:hidden shadow-sm"
                                    :style="'background-color:' + stage.color"
                                >
                                    <span x-text="stage.icon"></span>
                                </div>
                                <div class="flex-1">
                                    <div class="flex items-baseline gap-2">
                                        <span class="text-xs font-bold tracking-wider" :style="'color:' + stage.color" x-text="'0' + stage.order"></span>
                                        <h2 class="text-xl font-bold text-slate-900 tracking-tight" x-text="stage.label"></h2>
                                    </div>
                                    <p class="text-xs text-slate-400 mt-0.5">
                                        <span x-text="getStageToolCount(stage.id)"></span> 个工具
                                    </p>
                                </div>
                            </div>
                        </div>

                        <!-- 子分组 + 工具卡片 -->
                        <div class="space-y-5 ml-0 md:ml-2">
                            <template x-for="(subgroup, subKey) in getSubgroups(stage.id)" :key="subKey">
                                <div>
                                    <span
                                        class="subgroup-label"
                                        :style="'background-color:' + stage.color + '12; color:' + stage.color"
                                        x-text="subgroup.label"
                                    ></span>
                                    <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
                                        <template x-for="toolName in subgroup.tools" :key="toolName">
                                            <template x-if="getTool(toolName)">
                                                <div
                                                    class="tool-card"
                                                    :class="getToolCardClass(toolName)"
                                                    @click="openToolDetail(toolName)"
                                                    @mouseenter="highlightChain(toolName)"
                                                    @mouseleave="clearHighlight()"
                                                >
                                                    <!-- 顶部色条 -->
                                                    <div
                                                        class="absolute top-0 left-0 right-0 h-[3px] rounded-t-[14px]"
                                                        :style="'background: linear-gradient(90deg,' + stage.color + ',' + stage.color + '80)'"
                                                    ></div>

                                                    <div class="flex items-start justify-between mb-2.5">
                                                        <h4 class="font-semibold text-slate-800 text-sm leading-snug" x-text="getTool(toolName).display_name"></h4>
                                                        <span
                                                            class="text-[10px] px-2 py-0.5 rounded-md font-semibold shrink-0 ml-2 tracking-wide"
                                                            :class="getModeBadgeClass(getTool(toolName).launch_mode)"
                                                            x-text="getModeLabel(getTool(toolName))"
                                                        ></span>
                                                    </div>
                                                    <p class="text-[11px] text-slate-400 font-mono mb-1.5 tracking-tight" x-text="toolName"></p>
                                                    <p class="text-xs text-slate-500 line-clamp-2 leading-relaxed mb-3" x-text="getTool(toolName).description"></p>

                                                    <!-- Workflow 指示 -->
                                                    <div class="flex items-center gap-2 text-[11px]">
                                                        <template x-if="getTool(toolName).prev_tools.length">
                                                            <span class="flex items-center gap-0.5 text-emerald-500 font-medium">
                                                                <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"/></svg>
                                                                <span x-text="getTool(toolName).prev_tools.length"></span>
                                                            </span>
                                                        </template>
                                                        <template x-if="getTool(toolName).next_tools.length">
                                                            <span class="flex items-center gap-0.5 text-violet-500 font-medium">
                                                                <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>
                                                                <span x-text="getTool(toolName).next_tools.length"></span>
                                                            </span>
                                                        </template>
                                                    </div>

                                                    <!-- 使用场景（悬停展开） -->
                                                    <div class="use-cases-panel">
                                                        <div class="mt-3 pt-3 border-t border-slate-100 space-y-1.5">
                                                            <template x-for="(uc, i) in getTool(toolName).use_cases.slice(0, 2)" :key="i">
                                                                <p class="text-[11px] text-slate-400 leading-relaxed" x-text="uc"></p>
                                                            </template>
                                                        </div>
                                                    </div>

                                                    <!-- Tags -->
                                                    <div class="flex flex-wrap gap-1 mt-2.5">
                                                        <template x-for="tag in getTool(toolName).tags.slice(0, 2)" :key="tag">
                                                            <span class="text-[10px] bg-slate-50 text-slate-400 px-1.5 py-0.5 rounded-md font-medium" x-text="tag"></span>
                                                        </template>
                                                    </div>
                                                </div>
                                            </template>
                                        </template>
                                    </div>
                                </div>
                            </template>
                        </div>

                        <!-- 阶段间 Workflow 连线 -->
                        <template x-if="stage.order < 5">
                            <div class="flow-connector my-6 ml-0 md:ml-4">
                                <svg width="20" height="28" viewBox="0 0 20 28" fill="none">
                                    <path d="M10 0 L10 20 L5 15 M10 20 L15 15" :stroke="stage.color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.6"/>
                                </svg>
                            </div>
                        </template>
                    </div>
                </template>
            </div>
        </div>
    </div>

    <!-- 工具详情抽屉 -->
    <div
        x-show="showDetail"
        x-cloak
        class="fixed inset-0 z-50 flex justify-end"
        @keydown.escape.window="showDetail = false"
    >
        <!-- 遮罩 -->
        <div
            class="absolute inset-0 bg-slate-900/50 backdrop-blur-sm"
            @click="showDetail = false"
            x-transition:enter="transition ease-out duration-300"
            x-transition:enter-start="opacity-0"
            x-transition:enter-end="opacity-100"
            x-transition:leave="transition ease-in duration-200"
            x-transition:leave-start="opacity-100"
            x-transition:leave-end="opacity-0"
        ></div>
        <!-- 面板 -->
        <div
            class="drawer-panel relative w-full max-w-2xl bg-white shadow-2xl overflow-y-auto"
            :class="showDetail ? 'open' : ''"
        >
            <template x-if="detailTool">
                <div class="p-8">
                    <!-- 头部 -->
                    <div class="flex items-start justify-between mb-8">
                        <div>
                            <div class="flex items-center gap-3 mb-2">
                                <span class="text-2xl" x-text="getCategoryIcon(detailTool.category)"></span>
                                <h2 class="text-2xl font-bold text-slate-900 tracking-tight" x-text="detailTool.display_name"></h2>
                            </div>
                            <p class="text-slate-400 font-mono text-sm" x-text="detailTool.name"></p>
                        </div>
                        <button @click="showDetail = false" class="text-slate-400 hover:text-slate-600 p-2 rounded-xl hover:bg-slate-100 transition-all">
                            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                            </svg>
                        </button>
                    </div>

                    <!-- 描述 -->
                    <p class="text-slate-600 text-[15px] leading-relaxed mb-6" x-text="detailTool.description"></p>

                    <!-- 启动方式 -->
                    <div class="bg-slate-900 rounded-2xl p-5 mb-6">
                        <div class="flex items-center gap-2 mb-4">
                            <div class="w-2 h-2 rounded-full bg-emerald-400"></div>
                            <span class="text-slate-300 text-sm font-semibold tracking-wide">终端命令</span>
                        </div>
                        <div class="space-y-3">
                            <div>
                                <span class="text-slate-500 text-xs block mb-1.5 font-medium">安装</span>
                                <div class="flex items-center justify-between bg-slate-800/80 rounded-xl px-4 py-2.5">
                                    <code class="text-emerald-400 font-mono text-sm" x-text="detailTool.quick_start?.install || ('pip install ' + detailTool.name)"></code>
                                    <button @click="copyToClipboard(detailTool.quick_start?.install || ('pip install ' + detailTool.name))" class="text-slate-500 hover:text-white ml-3 text-sm transition-colors">📋</button>
                                </div>
                            </div>
                            <div>
                                <span class="text-slate-500 text-xs block mb-1.5 font-medium">运行</span>
                                <div class="flex items-center justify-between bg-slate-800/80 rounded-xl px-4 py-2.5">
                                    <code class="text-amber-400 font-mono text-sm" x-text="detailTool.quick_start?.command_example || detailTool.name"></code>
                                    <button @click="copyToClipboard(detailTool.quick_start?.command_example || detailTool.name)" class="text-slate-500 hover:text-white ml-3 text-sm transition-colors">📋</button>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Workflow -->
                    <div class="mb-6">
                        <h3 class="text-sm font-bold text-slate-700 mb-3 tracking-wide">工作流</h3>
                        <div class="flex flex-wrap items-center gap-2">
                            <template x-for="pt in detailTool.prev_tools" :key="pt">
                                <button @click="openToolDetail(pt)" class="px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-sm font-medium hover:bg-emerald-100 transition-colors border border-emerald-200/60" x-text="pt"></button>
                            </template>
                            <div class="px-3 py-1.5 bg-blue-600 text-white rounded-lg font-semibold text-sm shadow-sm shadow-blue-600/20" x-text="detailTool.name"></div>
                            <template x-for="nt in detailTool.next_tools" :key="nt">
                                <button @click="openToolDetail(nt)" class="px-3 py-1.5 bg-violet-50 text-violet-700 rounded-lg text-sm font-medium hover:bg-violet-100 transition-colors border border-violet-200/60" x-text="nt"></button>
                            </template>
                        </div>
                    </div>

                    <!-- 使用场景 -->
                    <div class="mb-6">
                        <h3 class="text-sm font-bold text-slate-700 mb-3 tracking-wide">使用场景</h3>
                        <div class="space-y-2">
                            <template x-for="(uc, i) in detailTool.use_cases" :key="i">
                                <div class="flex gap-3 p-3.5 bg-slate-50 rounded-xl border border-slate-100">
                                    <span class="text-blue-500 shrink-0 mt-0.5">
                                        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
                                    </span>
                                    <p class="text-slate-600 text-sm leading-relaxed" x-text="uc"></p>
                                </div>
                            </template>
                        </div>
                    </div>

                    <!-- 底部 -->
                    <div class="pt-5 border-t border-slate-100 flex items-center justify-between">
                        <div class="flex flex-wrap gap-1.5">
                            <template x-for="tag in detailTool.tags" :key="tag">
                                <span class="text-xs bg-slate-100 text-slate-500 px-2.5 py-1 rounded-lg font-medium" x-text="tag"></span>
                            </template>
                        </div>
                        <a :href="detailTool.pypi_url" target="_blank" class="text-sm text-blue-600 hover:text-blue-700 font-medium transition-colors">PyPI →</a>
                    </div>
                </div>
            </template>
        </div>
    </div>
</div>

<script>
function homeInfographic() {
    return {
        searchQuery: '',
        activeStage: 'prepare',
        activeScenario: null,
        highlightedTool: null,
        highlightedChain: [],
        showDetail: false,
        detailTool: null,
        allTools: {{ tools_json | safe }},
        stages: {{ stages_json | safe }},
        scenarios: {{ scenarios_json | safe }},
        subgroups: {{ subgroups_json | safe }},
        
        // 词云相关状态
        wordCloudWords: [],
        wordCloudActiveIndex: 0,
        wordCloudRotation: 0,
        wordCloudAnimation: true,
        wordCloudColors: [
            '#3B82F6', '#10B981', '#8B5CF6', '#F59E0B', '#06B6D4', 
            '#EF4444', '#EC4899', '#14B8A6', '#F97316', '#6366F1'
        ],
        wordCloudFontSizes: [18, 20, 22, 24, 26, 28, 30, 32, 34, 36],
        wordCloudRotationAngles: [-15, -10, -5, 0, 5, 10, 15],
        wordCloudCarouselInterval: null,
        wordCloudCarouselSpeed: 3000, // 3秒轮播

        // 工具名称到数据的映射
        get toolMap() {
            const m = {};
            this.allTools.forEach(t => m[t.name] = t);
            return m;
        },

        getTool(name) {
            return this.toolMap[name] || null;
        },

        getToolDisplayName(name) {
            const t = this.getTool(name);
            return t ? t.display_name : name;
        },

        getStageToolCount(stageId) {
            return this.allTools.filter(t => t.category === stageId).length;
        },

        getSubgroups(stageId) {
            return this.subgroups[stageId] || {};
        },

        get activeScenarioData() {
            if (!this.activeScenario) return null;
            return this.scenarios.find(s => s.id === this.activeScenario);
        },

        toggleScenario(id) {
            this.activeScenario = this.activeScenario === id ? null : id;
            this.highlightedChain = [];
            if (this.activeScenario) {
                const s = this.scenarios.find(sc => sc.id === id);
                if (s) this.highlightedChain = s.chain;
                // 更新词云
                this.updateWordCloud(s);
            } else {
                // 清空词云
                this.wordCloudWords = [];
                this.stopWordCloudCarousel();
            }
        },

        // 词云相关方法
        updateWordCloud(scenario) {
            if (!scenario || !scenario.chain) {
                this.wordCloudWords = [];
                return;
            }

            // 从场景链中提取关键词
            const keywords = scenario.chain.map(toolName => {
                const tool = this.getTool(toolName);
                return tool ? tool.display_name : toolName;
            });

            // 添加场景标题作为关键词
            keywords.unshift(scenario.title);

            // 生成词云数据，使用螺旋布局算法
            this.wordCloudWords = this.generateWordCloudLayout(keywords);

            // 开始轮播
            this.startWordCloudCarousel();
        },

        // 生成词云布局 - 使用螺旋布局和碰撞检测
        generateWordCloudLayout(keywords) {
            const words = keywords.map((word, index) => {
                const fontSize = this.wordCloudFontSizes[index % this.wordCloudFontSizes.length];
                const color = this.wordCloudColors[index % this.wordCloudColors.length];
                const rotation = this.wordCloudRotationAngles[index % this.wordCloudRotationAngles.length];

                return {
                    text: word,
                    fontSize: fontSize,
                    color: color,
                    rotation: rotation,
                    weight: Math.random() * 0.5 + 0.5,
                    x: 0,
                    y: 0,
                    width: 0,
                    height: 0,
                    active: index === 0
                };
            });

            // 使用螺旋布局算法计算位置
            this.calculateSpiralLayout(words);

            return words;
        },

        // 螺旋布局算法 - 避免重叠
        calculateSpiralLayout(words) {
            if (words.length === 0) return;

            // 动态获取容器尺寸
            const container = document.querySelector('.word-cloud-container');
            const containerWidth = container ? container.offsetWidth : 800;
            const containerHeight = container ? container.offsetHeight : 200;

            // 计算中心点
            const centerX = containerWidth / 2;
            const centerY = containerHeight / 2;

            // 估算文字宽度和高度
            words.forEach(word => {
                // 粗略估算文字宽度（中文字符算1.5倍宽度）
                const charCount = word.text.length;
                const chineseChars = (word.text.match(/[\u4e00-\u9fa5]/g) || []).length;
                const englishChars = charCount - chineseChars;
                word.width = (chineseChars * 1.5 + englishChars) * word.fontSize * 0.6;
                word.height = word.fontSize * 1.2;
            });

            // 螺旋布局参数
            const spiralStep = 0.5; // 螺旋步长
            const spiralRadius = 5; // 初始半径
            const maxIterations = 1000; // 最大迭代次数

            words.forEach((word, index) => {
                let placed = false;
                let angle = 0;
                let radius = spiralRadius;
                let iterations = 0;

                while (!placed && iterations < maxIterations) {
                    // 螺旋坐标 - 从中心开始
                    const x = centerX + radius * Math.cos(angle) - word.width / 2;
                    const y = centerY + radius * Math.sin(angle) - word.height / 2;

                    // 检查是否在容器内（留出边距）
                    const margin = 15;
                    if (x >= margin && x + word.width <= containerWidth - margin &&
                        y >= margin && y + word.height <= containerHeight - margin) {

                        // 检查是否与已放置的词重叠
                        let overlapping = false;
                        for (let i = 0; i < index; i++) {
                            const other = words[i];
                            if (this.checkOverlap(x, y, word.width, word.height,
                                other.x, other.y, other.width, other.height)) {
                                overlapping = true;
                                break;
                            }
                        }

                        if (!overlapping) {
                            word.x = x;
                            word.y = y;
                            placed = true;
                        }
                    }

                    // 增加螺旋参数
                    angle += spiralStep;
                    radius += 0.3; // 减小半径增长速度，让布局更紧凑
                    iterations++;
                }

                // 如果无法放置，使用网格布局作为后备
                if (!placed) {
                    const cols = Math.ceil(Math.sqrt(words.length));
                    const cellWidth = (containerWidth - margin * 2) / cols;
                    const cellHeight = (containerHeight - margin * 2) / Math.ceil(words.length / cols);
                    const col = index % cols;
                    const row = Math.floor(index / cols);
                    word.x = margin + col * cellWidth + (cellWidth - word.width) / 2;
                    word.y = margin + row * cellHeight + (cellHeight - word.height) / 2;
                }
            });
        },

        // 检查两个矩形是否重叠
        checkOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
            const padding = 8; // 词之间的最小间距
            return !(x1 + w1 + padding < x2 ||
                x2 + w2 + padding < x1 ||
                y1 + h1 + padding < y2 ||
                y2 + h2 + padding < y1);
        },

        startWordCloudCarousel() {
            this.stopWordCloudCarousel();
            
            if (this.wordCloudWords.length > 0 && this.wordCloudAnimation) {
                this.wordCloudCarouselInterval = setInterval(() => {
                    this.rotateWordCloud();
                }, this.wordCloudCarouselSpeed);
            }
        },

        stopWordCloudCarousel() {
            if (this.wordCloudCarouselInterval) {
                clearInterval(this.wordCloudCarouselInterval);
                this.wordCloudCarouselInterval = null;
            }
        },

        rotateWordCloud() {
            if (this.wordCloudWords.length === 0) return;

            // 先将所有词设为非激活状态
            this.wordCloudWords.forEach(word => {
                word.active = false;
            });

            // 设置当前激活的词
            this.wordCloudWords[this.wordCloudActiveIndex].active = true;

            // 更新激活索引（下一个词）
            this.wordCloudActiveIndex = (this.wordCloudActiveIndex + 1) % this.wordCloudWords.length;
        },

        toggleWordCloudAnimation() {
            this.wordCloudAnimation = !this.wordCloudAnimation;
            if (this.wordCloudAnimation) {
                this.startWordCloudCarousel();
            } else {
                this.stopWordCloudCarousel();
            }
        },

        selectWordCloudWord(index) {
            if (index >= 0 && index < this.wordCloudWords.length) {
                this.wordCloudActiveIndex = index;
                this.wordCloudWords.forEach((word, i) => {
                    word.active = i === index;
                });
            }
        },

        getWordCloudStyle(word) {
            const baseSize = word.fontSize;
            const scale = word.active ? 1.5 : 1; // 激活时放大1.5倍
            const opacity = word.active ? 1 : 0.6;
            const zIndex = word.active ? 100 : 1;

            return {
                left: `${word.x}px`,
                top: `${word.y}px`,
                fontSize: `${baseSize * scale}px`,
                color: word.color,
                transform: `rotate(${word.rotation}deg) scale(${scale})`,
                opacity: opacity,
                zIndex: zIndex,
                fontWeight: word.active ? '900' : '600',
                textShadow: word.active ?
                    `0 0 20px ${word.color}, 0 0 40px ${word.color}80, 0 4px 12px rgba(0, 0, 0, 0.3)` :
                    `0 2px 4px rgba(0, 0, 0, 0.1)`,
                transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)'
            };
        },

        getWordCloudPosition(index) {
            // 不再使用，位置已在calculateSpiralLayout中计算
            return {};
        },

        scrollToStage(stageId) {
            try {
                // 防止重复滚动
                if (this.isScrolling) return;
                
                this.activeStage = stageId;
                const el = document.getElementById('stage-' + stageId);
                
                if (!el) {
                    console.warn(`未找到ID为stage-${stageId}的元素`);
                    return;
                }
                
                this.isScrolling = true;
                
                // 计算滚动位置（考虑固定头部）
                const headerHeight = 80; // 头部高度
                const targetPosition = el.offsetTop - headerHeight - 20;
                const startPosition = window.pageYOffset;
                const distance = targetPosition - startPosition;
                const duration = 500; // 毫秒
                let startTime = null;
                
                // 平滑滚动动画
                const animateScroll = (currentTime) => {
                    if (!startTime) startTime = currentTime;
                    const timeElapsed = currentTime - startTime;
                    const progress = Math.min(timeElapsed / duration, 1);
                    
                    // 缓动函数
                    const easeInOutCubic = t => 
                        t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
                    
                    window.scrollTo(0, startPosition + distance * easeInOutCubic(progress));
                    
                    if (timeElapsed < duration) {
                        requestAnimationFrame(animateScroll);
                    } else {
                        this.isScrolling = false;
                        this.updateTimelineAccessibility();
                    }
                };
                
                requestAnimationFrame(animateScroll);
                
                // 更新可访问性状态
                this.updateTimelineAccessibility();
                
            } catch (error) {
                console.error('滚动到阶段失败:', error);
                this.isScrolling = false;
                
                // 回退到基本滚动
                const el = document.getElementById('stage-' + stageId);
                if (el) {
                    el.scrollIntoView({ behavior: 'smooth', block: 'start' });
                }
            }
        },
        
        // 处理键盘导航
        handleTimelineKeydown(event, stageId) {
            const stages = this.stages;
            const currentIndex = stages.findIndex(s => s.id === stageId);
            
            switch(event.key) {
                case 'Enter':
                case ' ':
                case 'Spacebar':
                    event.preventDefault();
                    this.scrollToStage(stageId);
                    break;
                    
                case 'ArrowUp':
                    event.preventDefault();
                    if (currentIndex > 0) {
                        const prevStage = stages[currentIndex - 1];
                        this.focusTimelineStage(prevStage.id);
                    }
                    break;
                    
                case 'ArrowDown':
                    event.preventDefault();
                    if (currentIndex < stages.length - 1) {
                        const nextStage = stages[currentIndex + 1];
                        this.focusTimelineStage(nextStage.id);
                    }
                    break;
                    
                case 'Home':
                    event.preventDefault();
                    this.focusTimelineStage(stages[0].id);
                    break;
                    
                case 'End':
                    event.preventDefault();
                    this.focusTimelineStage(stages[stages.length - 1].id);
                    break;
            }
        },
        
        // 聚焦时间线阶段
        focusTimelineStage(stageId) {
            const stageElement = document.querySelector(`[data-stage-id="${stageId}"]`);
            if (stageElement) {
                stageElement.focus();
                this.activeStage = stageId;
                this.updateTimelineAccessibility();
            }
        },
        
        // 更新时间线可访问性状态
        updateTimelineAccessibility() {
            // 更新所有时间线项目的ARIA状态
            const timelineItems = document.querySelectorAll('.timeline__item');
            timelineItems.forEach(item => {
                const stageId = item.dataset.stageId;
                const isActive = this.activeStage === stageId;
                
                item.setAttribute('aria-current', isActive ? 'true' : 'false');
                item.setAttribute('tabindex', isActive ? '0' : '-1');
                
                const dot = item.querySelector('.timeline__dot');
                if (dot) {
                    dot.setAttribute('aria-pressed', isActive ? 'true' : 'false');
                    dot.classList.toggle('timeline__dot--active', isActive);
                }
                
                const label = item.querySelector('.timeline__label');
                if (label) {
                    label.classList.toggle('timeline__label--active', isActive);
                    label.classList.toggle('timeline__label--inactive', !isActive);
                }
            });
        },
        
        // 防抖函数
        debounce(func, wait) {
            let timeout;
            return function executedFunction(...args) {
                const later = () => {
                    clearTimeout(timeout);
                    func(...args);
                };
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
            };
        },
        
        // 防抖的滚动监听
        debouncedUpdateActiveStage: null,
        
        // 设置滚动监听
        setupScrollListener() {
            this.debouncedUpdateActiveStage = this.debounce(() => {
                this.updateActiveStageFromScroll();
            }, 100);
            
            window.addEventListener('scroll', this.debouncedUpdateActiveStage);
        },
        
        // 从滚动位置更新激活阶段
        updateActiveStageFromScroll() {
            if (this.isScrolling) return;
            
            const viewportHeight = window.innerHeight;
            const scrollTop = window.pageYOffset;
            const scrollPosition = scrollTop + viewportHeight / 3;
            
            let closestStage = null;
            let minDistance = Infinity;
            
            this.stages.forEach(stage => {
                const targetElement = document.getElementById(`stage-${stage.id}`);
                if (targetElement) {
                    const elementTop = targetElement.offsetTop;
                    const elementBottom = elementTop + targetElement.offsetHeight;
                    const elementCenter = (elementTop + elementBottom) / 2;
                    const distance = Math.abs(scrollPosition - elementCenter);
                    
                    if (distance < minDistance) {
                        minDistance = distance;
                        closestStage = stage.id;
                    }
                }
            });
            
            if (closestStage && closestStage !== this.activeStage) {
                this.activeStage = closestStage;
                this.updateTimelineAccessibility();
            }
        },

        highlightChain(toolName) {
            if (this.activeScenario) return;
            const tool = this.getTool(toolName);
            if (!tool) return;
            this.highlightedTool = toolName;
            this.highlightedChain = [...tool.prev_tools, toolName, ...tool.next_tools];
        },

        highlightToolChain(toolName) {
            const tool = this.getTool(toolName);
            if (!tool) return;
            this.highlightedTool = toolName;
            this.highlightedChain = [...tool.prev_tools, toolName, ...tool.next_tools];
        },

        clearHighlight() {
            if (this.activeScenario) return;
            this.highlightedTool = null;
            this.highlightedChain = [];
        },

        getToolCardClass(toolName) {
            if (this.highlightedChain.length === 0) return '';
            return this.highlightedChain.includes(toolName) ? 'highlighted' : 'dimmed';
        },

        async openToolDetail(toolName) {
            try {
                const resp = await fetch(`/api/tool/${toolName}`);
                if (resp.ok) {
                    this.detailTool = await resp.json();
                    this.showDetail = true;
                }
            } catch (e) {
                console.error('Failed to load tool detail:', e);
            }
        },

        getCategoryIcon(category) {
            const icons = { search: '🔍', resume: '📄', outreach: '✉️', data: '📊', infra: '🔧' };
            return icons[category] || '📦';
        },

        getModeBadgeClass(mode) {
            const classes = {
                cli: 'bg-emerald-50 text-emerald-600 border border-emerald-200/60',
                web: 'bg-blue-50 text-blue-600 border border-blue-200/60',
                interactive: 'bg-violet-50 text-violet-600 border border-violet-200/60'
            };
            return classes[mode] || 'bg-slate-50 text-slate-600 border border-slate-200/60';
        },

        getModeLabel(tool) {
            if (tool.launch_mode === 'web' && tool.web_port) return 'Web:' + tool.web_port;
            return tool.launch_mode.toUpperCase();
        },

        copyToClipboard(text) {
            navigator.clipboard.writeText(text).then(() => {
                const btn = event.currentTarget;
                const original = btn.textContent;
                btn.textContent = '✅';
                setTimeout(() => { btn.textContent = original; }, 1500);
            });
        },

        // 初始化时间线
        init() {
            try {
                // 初始化状态
                this.isScrolling = false;
                
                // 初始化词云
                this.initWordCloud();
                
                // 设置滚动监听
                this.setupScrollListener();
                
                // 设置IntersectionObserver（如果可用）
                if ('IntersectionObserver' in window) {
                    this.setupIntersectionObserver();
                } else {
                    console.warn('浏览器不支持IntersectionObserver，使用滚动监听替代');
                }
                
                // 设置ResizeObserver（如果可用）
                if ('ResizeObserver' in window) {
                    this.setupResizeObserver();
                }
                
                // 初始化可访问性
                this.setupTimelineAccessibility();
                
                // 错误边界处理
                this.setupErrorBoundary();
                
            } catch (error) {
                console.error('时间线初始化失败:', error);
                this.fallbackToBasicNavigation();
            }
        },

        // 初始化词云
        initWordCloud() {
            try {
                // 如果有默认场景，初始化词云
                if (this.scenarios && this.scenarios.length > 0) {
                    // 延迟初始化，确保DOM已渲染
                    setTimeout(() => {
                        this.toggleScenario(this.scenarios[0].id);
                    }, 200);
                }

                // 监听窗口resize，重新计算词云位置
                let resizeTimeout;
                window.addEventListener('resize', () => {
                    clearTimeout(resizeTimeout);
                    resizeTimeout = setTimeout(() => {
                        // 如果有激活的场景，重新生成词云布局
                        if (this.activeScenario && this.wordCloudWords.length > 0) {
                            const scenario = this.scenarios.find(s => s.id === this.activeScenario);
                            if (scenario) {
                                this.updateWordCloud(scenario);
                            }
                        }
                    }, 300);
                });

            } catch (error) {
                console.error('词云初始化失败:', error);
            }
        },
        
        // 设置IntersectionObserver
        setupIntersectionObserver() {
            this.intersectionObserver = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && !this.isScrolling) {
                        const stageId = entry.target.id.replace('stage-', '');
                        this.activeStage = stageId;
                        this.updateTimelineAccessibility();
                    }
                });
            }, { 
                root: null,
                rootMargin: '-20% 0px -70% 0px', // 自定义视口阈值
                threshold: 0.1 
            });

            this.stages.forEach(stage => {
                const el = document.getElementById('stage-' + stage.id);
                if (el) this.intersectionObserver.observe(el);
            });
        },
        
        // 设置ResizeObserver
        setupResizeObserver() {
            const timelineContainer = document.querySelector('.timeline');
            if (!timelineContainer) return;
            
            this.resizeObserver = new ResizeObserver(() => {
                this.handleTimelineResize();
            });
            
            this.resizeObserver.observe(timelineContainer);
        },
        
        // 处理时间线resize
        handleTimelineResize() {
            const timelineContainer = document.querySelector('.timeline');
            if (!timelineContainer) return;
            
            const containerWidth = timelineContainer.offsetWidth;
            
            // 根据容器宽度调整布局
            if (containerWidth < 200) {
                timelineContainer.classList.add('timeline--compact');
                timelineContainer.classList.remove('timeline--normal');
            } else {
                timelineContainer.classList.add('timeline--normal');
                timelineContainer.classList.remove('timeline--compact');
            }
        },
        
        // 设置时间线可访问性
        setupTimelineAccessibility() {
            const timelineContainer = document.querySelector('.timeline');
            if (!timelineContainer) return;
            
            // 添加键盘导航说明
            let instructions = document.getElementById('timeline-instructions');
            if (!instructions) {
                instructions = document.createElement('div');
                instructions.id = 'timeline-instructions';
                instructions.className = 'sr-only';
                instructions.textContent = '使用Tab键导航时间线，使用上下箭头键切换阶段，按Enter或空格键跳转到对应阶段';
                timelineContainer.parentNode.insertBefore(instructions, timelineContainer);
            }
            
            // 初始更新可访问性状态
            this.updateTimelineAccessibility();
        },
        
        // 设置错误边界
        setupErrorBoundary() {
            const timelineContainer = document.querySelector('.timeline');
            if (!timelineContainer) return;
            
            // 添加错误处理
            window.addEventListener('error', (event) => {
                if (event.target && event.target.closest('.timeline')) {
                    console.error('时间线组件错误:', event.error);
                    this.showTimelineFallback();
                }
            });
            
            // 添加未处理的Promise rejection处理
            window.addEventListener('unhandledrejection', (event) => {
                console.error('时间线未处理的Promise rejection:', event.reason);
            });
        },
        
        // 显示时间线降级方案
        showTimelineFallback() {
            const timelineContainer = document.querySelector('.timeline');
            if (!timelineContainer || timelineContainer.classList.contains('timeline--error')) {
                return;
            }
            
            timelineContainer.classList.add('timeline--error');
            timelineContainer.innerHTML = `
                <div class="timeline-fallback">
                    <p>时间线加载失败，基本导航功能仍然可用。</p>
                    <button onclick="location.reload()" class="mt-2 px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200">
                        刷新页面
                    </button>
                </div>
            `;
        },
        
        // 回退到基本导航
        fallbackToBasicNavigation() {
            console.log('使用基本导航回退方案');
            
            const timelineItems = document.querySelectorAll('.timeline__item');
            timelineItems.forEach(item => {
                const stageId = item.dataset.stageId;
                const dot = item.querySelector('.timeline__dot');
                
                if (dot && stageId) {
                    dot.onclick = (e) => {
                        e.stopPropagation();
                        const target = document.getElementById(`stage-${stageId}`);
                        if (target) {
                            target.scrollIntoView({ behavior: 'smooth' });
                        }
                    };
                }
            });
            
            // 添加无JavaScript类
            const timelineContainer = document.querySelector('.timeline');
            if (timelineContainer) {
                timelineContainer.classList.add('timeline--no-js');
            }
        },
        
        // 清理资源
        destroy() {
            if (this.intersectionObserver) {
                this.intersectionObserver.disconnect();
            }
            
            if (this.resizeObserver) {
                this.resizeObserver.disconnect();
            }
            
            if (this.debouncedUpdateActiveStage) {
                window.removeEventListener('scroll', this.debouncedUpdateActiveStage);
            }
            
            // 清理词云资源
            this.stopWordCloudCarousel();
        }
    };
}
</script>
{% endblock %}

```