Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="env-setup-panel"> | |
| <div class="scroll-container"> | |
| <!-- Step 01: 模拟实例 --> | |
| <div class="step-card" :class="{ 'active': phase === 0, 'completed': phase > 0 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">01</span> | |
| <span class="step-title">模拟实例初始化</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="phase > 0" class="badge success">已完成</span> | |
| <span v-else class="badge processing">初始化</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/create</p> | |
| <p class="description"> | |
| 新建simulation实例,拉取模拟世界参数模版 | |
| </p> | |
| <div v-if="simulationId" class="info-card"> | |
| <div class="info-row"> | |
| <span class="info-label">Project ID</span> | |
| <span class="info-value mono">{{ projectData?.project_id }}</span> | |
| </div> | |
| <div class="info-row"> | |
| <span class="info-label">Graph ID</span> | |
| <span class="info-value mono">{{ projectData?.graph_id }}</span> | |
| </div> | |
| <div class="info-row"> | |
| <span class="info-label">Simulation ID</span> | |
| <span class="info-value mono">{{ simulationId }}</span> | |
| </div> | |
| <div class="info-row"> | |
| <span class="info-label">Task ID</span> | |
| <span class="info-value mono">{{ taskId || '异步任务已完成' }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 02: 生成 Agent 人设 --> | |
| <div class="step-card" :class="{ 'active': phase === 1, 'completed': phase > 1 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">02</span> | |
| <span class="step-title">生成 Agent 人设</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="phase > 1" class="badge success">已完成</span> | |
| <span v-else-if="phase === 1" class="badge processing">{{ prepareProgress }}%</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/prepare</p> | |
| <p class="description"> | |
| 结合上下文,自动调用工具从知识图谱梳理实体与关系,初始化模拟个体,并基于现实种子赋予他们独特的行为与记忆 | |
| </p> | |
| <!-- Profiles Stats --> | |
| <div v-if="profiles.length > 0" class="stats-grid"> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ profiles.length }}</span> | |
| <span class="stat-label">当前Agent数</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ expectedTotal || '-' }}</span> | |
| <span class="stat-label">预期Agent总数</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ totalTopicsCount }}</span> | |
| <span class="stat-label">现实种子当前关联话题数</span> | |
| </div> | |
| </div> | |
| <!-- Profiles List Preview --> | |
| <div v-if="profiles.length > 0" class="profiles-preview"> | |
| <div class="preview-header"> | |
| <span class="preview-title">已生成的 Agent 人设</span> | |
| </div> | |
| <div class="profiles-list"> | |
| <div | |
| v-for="(profile, idx) in profiles" | |
| :key="idx" | |
| class="profile-card" | |
| @click="selectProfile(profile)" | |
| > | |
| <div class="profile-header"> | |
| <span class="profile-realname">{{ profile.username || 'Unknown' }}</span> | |
| <span class="profile-username">@{{ profile.name || `agent_${idx}` }}</span> | |
| </div> | |
| <div class="profile-meta"> | |
| <span class="profile-profession">{{ profile.profession || '未知职业' }}</span> | |
| </div> | |
| <p class="profile-bio">{{ profile.bio || '暂无简介' }}</p> | |
| <div v-if="profile.interested_topics?.length" class="profile-topics"> | |
| <span | |
| v-for="topic in profile.interested_topics.slice(0, 3)" | |
| :key="topic" | |
| class="topic-tag" | |
| >{{ topic }}</span> | |
| <span v-if="profile.interested_topics.length > 3" class="topic-more"> | |
| +{{ profile.interested_topics.length - 3 }} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 03: 生成双平台模拟配置 --> | |
| <div class="step-card" :class="{ 'active': phase === 2, 'completed': phase > 2 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">03</span> | |
| <span class="step-title">生成双平台模拟配置</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="phase > 2" class="badge success">已完成</span> | |
| <span v-else-if="phase === 2" class="badge processing">生成中</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/prepare</p> | |
| <p class="description"> | |
| LLM 根据模拟需求与现实种子,智能设置世界时间流速、推荐算法、每个个体的活跃时间段、发言频率、事件触发等参数 | |
| </p> | |
| <!-- Config Preview --> | |
| <div v-if="simulationConfig" class="config-detail-panel"> | |
| <!-- 时间配置 --> | |
| <div class="config-block"> | |
| <div class="config-grid"> | |
| <div class="config-item"> | |
| <span class="config-item-label">模拟时长</span> | |
| <span class="config-item-value">{{ simulationConfig.time_config?.total_simulation_hours || '-' }} 小时</span> | |
| </div> | |
| <div class="config-item"> | |
| <span class="config-item-label">每轮时长</span> | |
| <span class="config-item-value">{{ simulationConfig.time_config?.minutes_per_round || '-' }} 分钟</span> | |
| </div> | |
| <div class="config-item"> | |
| <span class="config-item-label">总轮次</span> | |
| <span class="config-item-value">{{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} 轮</span> | |
| </div> | |
| <div class="config-item"> | |
| <span class="config-item-label">每小时活跃</span> | |
| <span class="config-item-value">{{ simulationConfig.time_config?.agents_per_hour_min }}-{{ simulationConfig.time_config?.agents_per_hour_max }}</span> | |
| </div> | |
| </div> | |
| <div class="time-periods"> | |
| <div class="period-item"> | |
| <span class="period-label">高峰时段</span> | |
| <span class="period-hours">{{ simulationConfig.time_config?.peak_hours?.join(':00, ') }}:00</span> | |
| <span class="period-multiplier">×{{ simulationConfig.time_config?.peak_activity_multiplier }}</span> | |
| </div> | |
| <div class="period-item"> | |
| <span class="period-label">工作时段</span> | |
| <span class="period-hours">{{ simulationConfig.time_config?.work_hours?.[0] }}:00-{{ simulationConfig.time_config?.work_hours?.slice(-1)[0] }}:00</span> | |
| <span class="period-multiplier">×{{ simulationConfig.time_config?.work_activity_multiplier }}</span> | |
| </div> | |
| <div class="period-item"> | |
| <span class="period-label">早间时段</span> | |
| <span class="period-hours">{{ simulationConfig.time_config?.morning_hours?.[0] }}:00-{{ simulationConfig.time_config?.morning_hours?.slice(-1)[0] }}:00</span> | |
| <span class="period-multiplier">×{{ simulationConfig.time_config?.morning_activity_multiplier }}</span> | |
| </div> | |
| <div class="period-item"> | |
| <span class="period-label">低谷时段</span> | |
| <span class="period-hours">{{ simulationConfig.time_config?.off_peak_hours?.[0] }}:00-{{ simulationConfig.time_config?.off_peak_hours?.slice(-1)[0] }}:00</span> | |
| <span class="period-multiplier">×{{ simulationConfig.time_config?.off_peak_activity_multiplier }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Agent 配置 --> | |
| <div class="config-block"> | |
| <div class="config-block-header"> | |
| <span class="config-block-title">Agent 配置</span> | |
| <span class="config-block-badge">{{ simulationConfig.agent_configs?.length || 0 }} 个</span> | |
| </div> | |
| <div class="agents-cards"> | |
| <div | |
| v-for="agent in simulationConfig.agent_configs" | |
| :key="agent.agent_id" | |
| class="agent-card" | |
| > | |
| <!-- 卡片头部 --> | |
| <div class="agent-card-header"> | |
| <div class="agent-identity"> | |
| <span class="agent-id">Agent {{ agent.agent_id }}</span> | |
| <span class="agent-name">{{ agent.entity_name }}</span> | |
| </div> | |
| <div class="agent-tags"> | |
| <span class="agent-type">{{ agent.entity_type }}</span> | |
| <span class="agent-stance" :class="'stance-' + agent.stance">{{ agent.stance }}</span> | |
| </div> | |
| </div> | |
| <!-- 活跃时间轴 --> | |
| <div class="agent-timeline"> | |
| <span class="timeline-label">活跃时段</span> | |
| <div class="mini-timeline"> | |
| <div | |
| v-for="hour in 24" | |
| :key="hour - 1" | |
| class="timeline-hour" | |
| :class="{ 'active': agent.active_hours?.includes(hour - 1) }" | |
| :title="`${hour - 1}:00`" | |
| ></div> | |
| </div> | |
| <div class="timeline-marks"> | |
| <span>0</span> | |
| <span>6</span> | |
| <span>12</span> | |
| <span>18</span> | |
| <span>24</span> | |
| </div> | |
| </div> | |
| <!-- 行为参数 --> | |
| <div class="agent-params"> | |
| <div class="param-group"> | |
| <div class="param-item"> | |
| <span class="param-label">发帖/时</span> | |
| <span class="param-value">{{ agent.posts_per_hour }}</span> | |
| </div> | |
| <div class="param-item"> | |
| <span class="param-label">评论/时</span> | |
| <span class="param-value">{{ agent.comments_per_hour }}</span> | |
| </div> | |
| <div class="param-item"> | |
| <span class="param-label">响应延迟</span> | |
| <span class="param-value">{{ agent.response_delay_min }}-{{ agent.response_delay_max }}min</span> | |
| </div> | |
| </div> | |
| <div class="param-group"> | |
| <div class="param-item"> | |
| <span class="param-label">活跃度</span> | |
| <span class="param-value with-bar"> | |
| <span class="mini-bar" :style="{ width: (agent.activity_level * 100) + '%' }"></span> | |
| {{ (agent.activity_level * 100).toFixed(0) }}% | |
| </span> | |
| </div> | |
| <div class="param-item"> | |
| <span class="param-label">情感倾向</span> | |
| <span class="param-value" :class="agent.sentiment_bias > 0 ? 'positive' : agent.sentiment_bias < 0 ? 'negative' : 'neutral'"> | |
| {{ agent.sentiment_bias > 0 ? '+' : '' }}{{ agent.sentiment_bias?.toFixed(1) }} | |
| </span> | |
| </div> | |
| <div class="param-item"> | |
| <span class="param-label">影响力</span> | |
| <span class="param-value highlight">{{ agent.influence_weight?.toFixed(1) }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 平台配置 --> | |
| <div class="config-block"> | |
| <div class="config-block-header"> | |
| <span class="config-block-title">推荐算法配置</span> | |
| </div> | |
| <div class="platforms-grid"> | |
| <div v-if="simulationConfig.twitter_config" class="platform-card"> | |
| <div class="platform-card-header"> | |
| <span class="platform-name">平台 1:广场 / 信息流</span> | |
| </div> | |
| <div class="platform-params"> | |
| <div class="param-row"> | |
| <span class="param-label">时效权重</span> | |
| <span class="param-value">{{ simulationConfig.twitter_config.recency_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">热度权重</span> | |
| <span class="param-value">{{ simulationConfig.twitter_config.popularity_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">相关性权重</span> | |
| <span class="param-value">{{ simulationConfig.twitter_config.relevance_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">病毒阈值</span> | |
| <span class="param-value">{{ simulationConfig.twitter_config.viral_threshold }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">回音室强度</span> | |
| <span class="param-value">{{ simulationConfig.twitter_config.echo_chamber_strength }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-if="simulationConfig.reddit_config" class="platform-card"> | |
| <div class="platform-card-header"> | |
| <span class="platform-name">平台 2:话题 / 社区</span> | |
| </div> | |
| <div class="platform-params"> | |
| <div class="param-row"> | |
| <span class="param-label">时效权重</span> | |
| <span class="param-value">{{ simulationConfig.reddit_config.recency_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">热度权重</span> | |
| <span class="param-value">{{ simulationConfig.reddit_config.popularity_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">相关性权重</span> | |
| <span class="param-value">{{ simulationConfig.reddit_config.relevance_weight }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">病毒阈值</span> | |
| <span class="param-value">{{ simulationConfig.reddit_config.viral_threshold }}</span> | |
| </div> | |
| <div class="param-row"> | |
| <span class="param-label">回音室强度</span> | |
| <span class="param-value">{{ simulationConfig.reddit_config.echo_chamber_strength }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- LLM 配置推理 --> | |
| <div v-if="simulationConfig.generation_reasoning" class="config-block"> | |
| <div class="config-block-header"> | |
| <span class="config-block-title">LLM 配置推理</span> | |
| </div> | |
| <div class="reasoning-content"> | |
| <div | |
| v-for="(reason, idx) in simulationConfig.generation_reasoning.split('|').slice(0, 2)" | |
| :key="idx" | |
| class="reasoning-item" | |
| > | |
| <p class="reasoning-text">{{ reason.trim() }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 04: 初始激活编排 --> | |
| <div class="step-card" :class="{ 'active': phase === 3, 'completed': phase > 3 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">04</span> | |
| <span class="step-title">初始激活编排</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="phase > 3" class="badge success">已完成</span> | |
| <span v-else-if="phase === 3" class="badge processing">编排中</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/prepare</p> | |
| <p class="description"> | |
| 基于叙事方向,自动生成初始激活事件与热点话题,引导模拟世界的初始状态 | |
| </p> | |
| <div v-if="simulationConfig?.event_config" class="orchestration-content"> | |
| <!-- 叙事方向 --> | |
| <div class="narrative-box"> | |
| <span class="box-label narrative-label"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="special-icon"> | |
| <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="url(#paint0_linear)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M16.24 7.76L14.12 14.12L7.76 16.24L9.88 9.88L16.24 7.76Z" fill="url(#paint0_linear)" stroke="url(#paint0_linear)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| <defs> | |
| <linearGradient id="paint0_linear" x1="2" y1="2" x2="22" y2="22" gradientUnits="userSpaceOnUse"> | |
| <stop stop-color="#FF5722"/> | |
| <stop offset="1" stop-color="#FF9800"/> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| 叙事引导方向 | |
| </span> | |
| <p class="narrative-text">{{ simulationConfig.event_config.narrative_direction }}</p> | |
| </div> | |
| <!-- 热点话题 --> | |
| <div class="topics-section"> | |
| <span class="box-label">初始热点话题</span> | |
| <div class="hot-topics-grid"> | |
| <span v-for="topic in simulationConfig.event_config.hot_topics" :key="topic" class="hot-topic-tag"> | |
| # {{ topic }} | |
| </span> | |
| </div> | |
| </div> | |
| <!-- 初始帖子流 --> | |
| <div class="initial-posts-section"> | |
| <span class="box-label">初始激活序列 ({{ simulationConfig.event_config.initial_posts.length }})</span> | |
| <div class="posts-timeline"> | |
| <div v-for="(post, idx) in simulationConfig.event_config.initial_posts" :key="idx" class="timeline-item"> | |
| <div class="timeline-marker"></div> | |
| <div class="timeline-content"> | |
| <div class="post-header"> | |
| <span class="post-role">{{ post.poster_type }}</span> | |
| <span class="post-agent-info"> | |
| <span class="post-id">Agent {{ post.poster_agent_id }}</span> | |
| <span class="post-username">@{{ getAgentUsername(post.poster_agent_id) }}</span> | |
| </span> | |
| </div> | |
| <p class="post-text">{{ post.content }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 05: 准备完成 --> | |
| <div class="step-card" :class="{ 'active': phase === 4 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">05</span> | |
| <span class="step-title">准备完成</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="phase >= 4" class="badge processing">进行中</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/start</p> | |
| <p class="description">模拟环境已准备完成,可以开始运行模拟</p> | |
| <!-- 模拟轮数配置 - 只有在配置生成完成且轮数计算出来后才显示 --> | |
| <div v-if="simulationConfig && autoGeneratedRounds" class="rounds-config-section"> | |
| <div class="rounds-header"> | |
| <div class="header-left"> | |
| <span class="section-title">模拟轮数设定</span> | |
| <span class="section-desc">MiroFish 自动规划推演现实 <span class="desc-highlight">{{ simulationConfig?.time_config?.total_simulation_hours || '-' }}</span> 小时,每轮代表现实 <span class="desc-highlight">{{ simulationConfig?.time_config?.minutes_per_round || '-' }}</span> 分钟时间流逝</span> | |
| </div> | |
| <label class="switch-control"> | |
| <input type="checkbox" v-model="useCustomRounds"> | |
| <span class="switch-track"></span> | |
| <span class="switch-label">自定义</span> | |
| </label> | |
| </div> | |
| <Transition name="fade" mode="out-in"> | |
| <div v-if="useCustomRounds" class="rounds-content custom" key="custom"> | |
| <div class="slider-display"> | |
| <div class="slider-main-value"> | |
| <span class="val-num">{{ customMaxRounds }}</span> | |
| <span class="val-unit">轮</span> | |
| </div> | |
| <div class="slider-meta-info"> | |
| <span>若Agent规模为100:预计耗时约 {{ Math.round(customMaxRounds * 0.6) }} 分钟</span> | |
| </div> | |
| </div> | |
| <div class="range-wrapper"> | |
| <input | |
| type="range" | |
| v-model.number="customMaxRounds" | |
| min="10" | |
| :max="autoGeneratedRounds" | |
| step="5" | |
| class="minimal-slider" | |
| :style="{ '--percent': ((customMaxRounds - 10) / (autoGeneratedRounds - 10)) * 100 + '%' }" | |
| /> | |
| <div class="range-marks"> | |
| <span>10</span> | |
| <span | |
| class="mark-recommend" | |
| :class="{ active: customMaxRounds === 40 }" | |
| @click="customMaxRounds = 40" | |
| :style="{ position: 'absolute', left: `calc(${(40 - 10) / (autoGeneratedRounds - 10) * 100}% - 30px)` }" | |
| >40 (推荐)</span> | |
| <span>{{ autoGeneratedRounds }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-else class="rounds-content auto" key="auto"> | |
| <div class="auto-info-card"> | |
| <div class="auto-value"> | |
| <span class="val-num">{{ autoGeneratedRounds }}</span> | |
| <span class="val-unit">轮</span> | |
| </div> | |
| <div class="auto-content"> | |
| <div class="auto-meta-row"> | |
| <span class="duration-badge"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <polyline points="12 6 12 12 16 14"></polyline> | |
| </svg> | |
| 若Agent规模为100:预计耗时 {{ Math.round(autoGeneratedRounds * 0.6) }} 分钟 | |
| </span> | |
| </div> | |
| <div class="auto-desc"> | |
| <p class="highlight-tip" @click="useCustomRounds = true">若首次运行,强烈建议切换至‘自定义模式’减少模拟轮数,以便快速预览效果并降低报错风险 ➝</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </Transition> | |
| </div> | |
| <div class="action-group dual"> | |
| <button | |
| class="action-btn secondary" | |
| @click="$emit('go-back')" | |
| > | |
| ← 返回图谱构建 | |
| </button> | |
| <button | |
| class="action-btn primary" | |
| :disabled="phase < 4" | |
| @click="handleStartSimulation" | |
| > | |
| 开始双世界并行模拟 ➝ | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Profile Detail Modal --> | |
| <Transition name="modal"> | |
| <div v-if="selectedProfile" class="profile-modal-overlay" @click.self="selectedProfile = null"> | |
| <div class="profile-modal"> | |
| <div class="modal-header"> | |
| <div class="modal-header-info"> | |
| <div class="modal-name-row"> | |
| <span class="modal-realname">{{ selectedProfile.username }}</span> | |
| <span class="modal-username">@{{ selectedProfile.name }}</span> | |
| </div> | |
| <span class="modal-profession">{{ selectedProfile.profession }}</span> | |
| </div> | |
| <button class="close-btn" @click="selectedProfile = null">×</button> | |
| </div> | |
| <div class="modal-body"> | |
| <!-- 基本信息 --> | |
| <div class="modal-info-grid"> | |
| <div class="info-item"> | |
| <span class="info-label">事件外显年龄</span> | |
| <span class="info-value">{{ selectedProfile.age || '-' }} 岁</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">事件外显性别</span> | |
| <span class="info-value">{{ { male: '男', female: '女', other: '其他' }[selectedProfile.gender] || selectedProfile.gender }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">国家/地区</span> | |
| <span class="info-value">{{ selectedProfile.country || '-' }}</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">事件外显MBTI</span> | |
| <span class="info-value mbti">{{ selectedProfile.mbti || '-' }}</span> | |
| </div> | |
| </div> | |
| <!-- 简介 --> | |
| <div class="modal-section"> | |
| <span class="section-label">人设简介</span> | |
| <p class="section-bio">{{ selectedProfile.bio || '暂无简介' }}</p> | |
| </div> | |
| <!-- 关注话题 --> | |
| <div class="modal-section" v-if="selectedProfile.interested_topics?.length"> | |
| <span class="section-label">现实种子关联话题</span> | |
| <div class="topics-grid"> | |
| <span | |
| v-for="topic in selectedProfile.interested_topics" | |
| :key="topic" | |
| class="topic-item" | |
| >{{ topic }}</span> | |
| </div> | |
| </div> | |
| <!-- 详细人设 --> | |
| <div class="modal-section" v-if="selectedProfile.persona"> | |
| <span class="section-label">详细人设背景</span> | |
| <!-- 人设维度概览 --> | |
| <div class="persona-dimensions"> | |
| <div class="dimension-card"> | |
| <span class="dim-title">事件全景经历</span> | |
| <span class="dim-desc">在此事件中的完整行为轨迹</span> | |
| </div> | |
| <div class="dimension-card"> | |
| <span class="dim-title">行为模式侧写</span> | |
| <span class="dim-desc">经验总结与行事风格偏好</span> | |
| </div> | |
| <div class="dimension-card"> | |
| <span class="dim-title">独特记忆印记</span> | |
| <span class="dim-desc">基于现实种子形成的记忆</span> | |
| </div> | |
| <div class="dimension-card"> | |
| <span class="dim-title">社会关系网络</span> | |
| <span class="dim-desc">个体链接与交互图谱</span> | |
| </div> | |
| </div> | |
| <div class="persona-content"> | |
| <p class="section-persona">{{ selectedProfile.persona }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </Transition> | |
| <!-- Bottom Info / Logs --> | |
| <div class="system-logs"> | |
| <div class="log-header"> | |
| <span class="log-title">SYSTEM DASHBOARD</span> | |
| <span class="log-id">{{ simulationId || 'NO_SIMULATION' }}</span> | |
| </div> | |
| <div class="log-content" ref="logContent"> | |
| <div class="log-line" v-for="(log, idx) in systemLogs" :key="idx"> | |
| <span class="log-time">{{ log.time }}</span> | |
| <span class="log-msg">{{ log.msg }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue' | |
| import { | |
| prepareSimulation, | |
| getPrepareStatus, | |
| getSimulationProfilesRealtime, | |
| getSimulationConfig, | |
| getSimulationConfigRealtime | |
| } from '../api/simulation' | |
| const props = defineProps({ | |
| simulationId: String, // 从父组件传入 | |
| projectData: Object, | |
| graphData: Object, | |
| systemLogs: Array | |
| }) | |
| const emit = defineEmits(['go-back', 'next-step', 'add-log', 'update-status']) | |
| // State | |
| const phase = ref(0) // 0: 初始化, 1: 生成人设, 2: 生成配置, 3: 完成 | |
| const taskId = ref(null) | |
| const prepareProgress = ref(0) | |
| const currentStage = ref('') | |
| const progressMessage = ref('') | |
| const profiles = ref([]) | |
| const entityTypes = ref([]) | |
| const expectedTotal = ref(null) | |
| const simulationConfig = ref(null) | |
| const selectedProfile = ref(null) | |
| const showProfilesDetail = ref(true) | |
| // 日志去重:记录上一次输出的关键信息 | |
| let lastLoggedMessage = '' | |
| let lastLoggedProfileCount = 0 | |
| let lastLoggedConfigStage = '' | |
| // 模拟轮数配置 | |
| const useCustomRounds = ref(false) // 默认使用自动配置轮数 | |
| const customMaxRounds = ref(40) // 默认推荐40轮 | |
| // Watch stage to update phase | |
| watch(currentStage, (newStage) => { | |
| if (newStage === '生成Agent人设' || newStage === 'generating_profiles') { | |
| phase.value = 1 | |
| } else if (newStage === '生成模拟配置' || newStage === 'generating_config') { | |
| phase.value = 2 | |
| // 进入配置生成阶段,开始轮询配置 | |
| if (!configTimer) { | |
| addLog('开始生成双平台模拟配置...') | |
| startConfigPolling() | |
| } | |
| } else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') { | |
| phase.value = 2 // 仍属于配置阶段 | |
| } | |
| }) | |
| // 从配置中计算自动生成的轮数(不使用硬编码默认值) | |
| const autoGeneratedRounds = computed(() => { | |
| if (!simulationConfig.value?.time_config) { | |
| return null // 配置未生成时返回 null | |
| } | |
| const totalHours = simulationConfig.value.time_config.total_simulation_hours | |
| const minutesPerRound = simulationConfig.value.time_config.minutes_per_round | |
| if (!totalHours || !minutesPerRound) { | |
| return null // 配置数据不完整时返回 null | |
| } | |
| const calculatedRounds = Math.floor((totalHours * 60) / minutesPerRound) | |
| // 确保最大轮数不小于40(推荐值),避免滑动条范围异常 | |
| return Math.max(calculatedRounds, 40) | |
| }) | |
| // Polling timer | |
| let pollTimer = null | |
| let profilesTimer = null | |
| let configTimer = null | |
| // Computed | |
| const displayProfiles = computed(() => { | |
| if (showProfilesDetail.value) { | |
| return profiles.value | |
| } | |
| return profiles.value.slice(0, 6) | |
| }) | |
| // 根据agent_id获取对应的username | |
| const getAgentUsername = (agentId) => { | |
| if (profiles.value && profiles.value.length > agentId && agentId >= 0) { | |
| const profile = profiles.value[agentId] | |
| return profile?.username || `agent_${agentId}` | |
| } | |
| return `agent_${agentId}` | |
| } | |
| // 计算所有人设的关联话题总数 | |
| const totalTopicsCount = computed(() => { | |
| return profiles.value.reduce((sum, p) => { | |
| return sum + (p.interested_topics?.length || 0) | |
| }, 0) | |
| }) | |
| // Methods | |
| const addLog = (msg) => { | |
| emit('add-log', msg) | |
| } | |
| // 处理开始模拟按钮点击 | |
| const handleStartSimulation = () => { | |
| // 构建传递给父组件的参数 | |
| const params = {} | |
| if (useCustomRounds.value) { | |
| // 用户自定义轮数,传递 max_rounds 参数 | |
| params.maxRounds = customMaxRounds.value | |
| addLog(`开始模拟,自定义轮数: ${customMaxRounds.value} 轮`) | |
| } else { | |
| // 用户选择保持自动生成的轮数,不传递 max_rounds 参数 | |
| addLog(`开始模拟,使用自动配置轮数: ${autoGeneratedRounds.value} 轮`) | |
| } | |
| emit('next-step', params) | |
| } | |
| const truncateBio = (bio) => { | |
| if (bio.length > 80) { | |
| return bio.substring(0, 80) + '...' | |
| } | |
| return bio | |
| } | |
| const selectProfile = (profile) => { | |
| selectedProfile.value = profile | |
| } | |
| // 自动开始准备模拟 | |
| const startPrepareSimulation = async () => { | |
| if (!props.simulationId) { | |
| addLog('错误:缺少 simulationId') | |
| emit('update-status', 'error') | |
| return | |
| } | |
| // 标记第一步完成,开始第二步 | |
| phase.value = 1 | |
| addLog(`模拟实例已创建: ${props.simulationId}`) | |
| addLog('正在准备模拟环境...') | |
| emit('update-status', 'processing') | |
| try { | |
| const res = await prepareSimulation({ | |
| simulation_id: props.simulationId, | |
| use_llm_for_profiles: true, | |
| parallel_profile_count: 5 | |
| }) | |
| if (res.success && res.data) { | |
| if (res.data.already_prepared) { | |
| addLog('检测到已有完成的准备工作,直接使用') | |
| await loadPreparedData() | |
| return | |
| } | |
| taskId.value = res.data.task_id | |
| addLog(`准备任务已启动`) | |
| addLog(` └─ Task ID: ${res.data.task_id}`) | |
| // 立即设置预期Agent总数(从prepare接口返回值获取) | |
| if (res.data.expected_entities_count) { | |
| expectedTotal.value = res.data.expected_entities_count | |
| addLog(`从Zep图谱读取到 ${res.data.expected_entities_count} 个实体`) | |
| if (res.data.entity_types && res.data.entity_types.length > 0) { | |
| addLog(` └─ 实体类型: ${res.data.entity_types.join(', ')}`) | |
| } | |
| } | |
| addLog('开始轮询准备进度...') | |
| // 开始轮询进度 | |
| startPolling() | |
| // 开始实时获取 Profiles | |
| startProfilesPolling() | |
| } else { | |
| addLog(`准备失败: ${res.error || '未知错误'}`) | |
| emit('update-status', 'error') | |
| } | |
| } catch (err) { | |
| addLog(`准备异常: ${err.message}`) | |
| emit('update-status', 'error') | |
| } | |
| } | |
| const startPolling = () => { | |
| pollTimer = setInterval(pollPrepareStatus, 2000) | |
| } | |
| const stopPolling = () => { | |
| if (pollTimer) { | |
| clearInterval(pollTimer) | |
| pollTimer = null | |
| } | |
| } | |
| const startProfilesPolling = () => { | |
| profilesTimer = setInterval(fetchProfilesRealtime, 3000) | |
| } | |
| const stopProfilesPolling = () => { | |
| if (profilesTimer) { | |
| clearInterval(profilesTimer) | |
| profilesTimer = null | |
| } | |
| } | |
| const pollPrepareStatus = async () => { | |
| if (!taskId.value && !props.simulationId) return | |
| try { | |
| const res = await getPrepareStatus({ | |
| task_id: taskId.value, | |
| simulation_id: props.simulationId | |
| }) | |
| if (res.success && res.data) { | |
| const data = res.data | |
| // 更新进度 | |
| prepareProgress.value = data.progress || 0 | |
| progressMessage.value = data.message || '' | |
| // 解析阶段信息并输出详细日志 | |
| if (data.progress_detail) { | |
| currentStage.value = data.progress_detail.current_stage_name || '' | |
| // 输出详细进度日志(避免重复) | |
| const detail = data.progress_detail | |
| const logKey = `${detail.current_stage}-${detail.current_item}-${detail.total_items}` | |
| if (logKey !== lastLoggedMessage && detail.item_description) { | |
| lastLoggedMessage = logKey | |
| const stageInfo = `[${detail.stage_index}/${detail.total_stages}]` | |
| if (detail.total_items > 0) { | |
| addLog(`${stageInfo} ${detail.current_stage_name}: ${detail.current_item}/${detail.total_items} - ${detail.item_description}`) | |
| } else { | |
| addLog(`${stageInfo} ${detail.current_stage_name}: ${detail.item_description}`) | |
| } | |
| } | |
| } else if (data.message) { | |
| // 从消息中提取阶段 | |
| const match = data.message.match(/\[(\d+)\/(\d+)\]\s*([^:]+)/) | |
| if (match) { | |
| currentStage.value = match[3].trim() | |
| } | |
| // 输出消息日志(避免重复) | |
| if (data.message !== lastLoggedMessage) { | |
| lastLoggedMessage = data.message | |
| addLog(data.message) | |
| } | |
| } | |
| // 检查是否完成 | |
| if (data.status === 'completed' || data.status === 'ready' || data.already_prepared) { | |
| addLog('✓ 准备工作已完成') | |
| stopPolling() | |
| stopProfilesPolling() | |
| await loadPreparedData() | |
| } else if (data.status === 'failed') { | |
| addLog(`✗ 准备失败: ${data.error || '未知错误'}`) | |
| stopPolling() | |
| stopProfilesPolling() | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('轮询状态失败:', err) | |
| } | |
| } | |
| const fetchProfilesRealtime = async () => { | |
| if (!props.simulationId) return | |
| try { | |
| const res = await getSimulationProfilesRealtime(props.simulationId, 'reddit') | |
| if (res.success && res.data) { | |
| const prevCount = profiles.value.length | |
| profiles.value = res.data.profiles || [] | |
| // 只有当 API 返回有效值时才更新,避免覆盖已有的有效值 | |
| if (res.data.total_expected) { | |
| expectedTotal.value = res.data.total_expected | |
| } | |
| // 提取实体类型 | |
| const types = new Set() | |
| profiles.value.forEach(p => { | |
| if (p.entity_type) types.add(p.entity_type) | |
| }) | |
| entityTypes.value = Array.from(types) | |
| // 输出 Profile 生成进度日志(仅当数量变化时) | |
| const currentCount = profiles.value.length | |
| if (currentCount > 0 && currentCount !== lastLoggedProfileCount) { | |
| lastLoggedProfileCount = currentCount | |
| const total = expectedTotal.value || '?' | |
| const latestProfile = profiles.value[currentCount - 1] | |
| const profileName = latestProfile?.name || latestProfile?.username || `Agent_${currentCount}` | |
| if (currentCount === 1) { | |
| addLog(`开始生成Agent人设...`) | |
| } | |
| addLog(`→ Agent人设 ${currentCount}/${total}: ${profileName} (${latestProfile?.profession || '未知职业'})`) | |
| // 如果全部生成完成 | |
| if (expectedTotal.value && currentCount >= expectedTotal.value) { | |
| addLog(`✓ 全部 ${currentCount} 个Agent人设生成完成`) | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('获取 Profiles 失败:', err) | |
| } | |
| } | |
| // 配置轮询 | |
| const startConfigPolling = () => { | |
| configTimer = setInterval(fetchConfigRealtime, 2000) | |
| } | |
| const stopConfigPolling = () => { | |
| if (configTimer) { | |
| clearInterval(configTimer) | |
| configTimer = null | |
| } | |
| } | |
| const fetchConfigRealtime = async () => { | |
| if (!props.simulationId) return | |
| try { | |
| const res = await getSimulationConfigRealtime(props.simulationId) | |
| if (res.success && res.data) { | |
| const data = res.data | |
| // 输出配置生成阶段日志(避免重复) | |
| if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) { | |
| lastLoggedConfigStage = data.generation_stage | |
| if (data.generation_stage === 'generating_profiles') { | |
| addLog('正在生成Agent人设配置...') | |
| } else if (data.generation_stage === 'generating_config') { | |
| addLog('正在调用LLM生成模拟配置参数...') | |
| } | |
| } | |
| // 如果配置已生成 | |
| if (data.config_generated && data.config) { | |
| simulationConfig.value = data.config | |
| addLog('✓ 模拟配置生成完成') | |
| // 显示详细配置摘要 | |
| if (data.summary) { | |
| addLog(` ├─ Agent数量: ${data.summary.total_agents}个`) | |
| addLog(` ├─ 模拟时长: ${data.summary.simulation_hours}小时`) | |
| addLog(` ├─ 初始帖子: ${data.summary.initial_posts_count}条`) | |
| addLog(` ├─ 热点话题: ${data.summary.hot_topics_count}个`) | |
| addLog(` └─ 平台配置: Twitter ${data.summary.has_twitter_config ? '✓' : '✗'}, Reddit ${data.summary.has_reddit_config ? '✓' : '✗'}`) | |
| } | |
| // 显示时间配置详情 | |
| if (data.config.time_config) { | |
| const tc = data.config.time_config | |
| addLog(`时间配置: 每轮${tc.minutes_per_round}分钟, 共${Math.floor((tc.total_simulation_hours * 60) / tc.minutes_per_round)}轮`) | |
| } | |
| // 显示事件配置 | |
| if (data.config.event_config?.narrative_direction) { | |
| const narrative = data.config.event_config.narrative_direction | |
| addLog(`叙事方向: ${narrative.length > 50 ? narrative.substring(0, 50) + '...' : narrative}`) | |
| } | |
| stopConfigPolling() | |
| phase.value = 4 | |
| addLog('✓ 环境搭建完成,可以开始模拟') | |
| emit('update-status', 'completed') | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('获取 Config 失败:', err) | |
| } | |
| } | |
| const loadPreparedData = async () => { | |
| phase.value = 2 | |
| addLog('正在加载已有配置数据...') | |
| // 最后获取一次 Profiles | |
| await fetchProfilesRealtime() | |
| addLog(`已加载 ${profiles.value.length} 个Agent人设`) | |
| // 获取配置(使用实时接口) | |
| try { | |
| const res = await getSimulationConfigRealtime(props.simulationId) | |
| if (res.success && res.data) { | |
| if (res.data.config_generated && res.data.config) { | |
| simulationConfig.value = res.data.config | |
| addLog('✓ 模拟配置加载成功') | |
| // 显示详细配置摘要 | |
| if (res.data.summary) { | |
| addLog(` ├─ Agent数量: ${res.data.summary.total_agents}个`) | |
| addLog(` ├─ 模拟时长: ${res.data.summary.simulation_hours}小时`) | |
| addLog(` └─ 初始帖子: ${res.data.summary.initial_posts_count}条`) | |
| } | |
| addLog('✓ 环境搭建完成,可以开始模拟') | |
| phase.value = 4 | |
| emit('update-status', 'completed') | |
| } else { | |
| // 配置尚未生成,开始轮询 | |
| addLog('配置生成中,开始轮询等待...') | |
| startConfigPolling() | |
| } | |
| } | |
| } catch (err) { | |
| addLog(`加载配置失败: ${err.message}`) | |
| emit('update-status', 'error') | |
| } | |
| } | |
| // Scroll log to bottom | |
| const logContent = ref(null) | |
| watch(() => props.systemLogs?.length, () => { | |
| nextTick(() => { | |
| if (logContent.value) { | |
| logContent.value.scrollTop = logContent.value.scrollHeight | |
| } | |
| }) | |
| }) | |
| onMounted(() => { | |
| // 自动开始准备流程 | |
| if (props.simulationId) { | |
| addLog('Step2 环境搭建初始化') | |
| startPrepareSimulation() | |
| } | |
| }) | |
| onUnmounted(() => { | |
| stopPolling() | |
| stopProfilesPolling() | |
| stopConfigPolling() | |
| }) | |
| </script> | |
| <style scoped> | |
| .env-setup-panel { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| background: #FAFAFA; | |
| font-family: 'Space Grotesk', 'Noto Sans SC', system-ui, sans-serif; | |
| } | |
| .scroll-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| /* Step Card */ | |
| .step-card { | |
| background: #FFF; | |
| border-radius: 8px; | |
| padding: 20px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.04); | |
| border: 1px solid #EAEAEA; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| } | |
| .step-card.active { | |
| border-color: #FF5722; | |
| box-shadow: 0 4px 12px rgba(255, 87, 34, 0.08); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| } | |
| .step-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .step-num { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #E0E0E0; | |
| } | |
| .step-card.active .step-num, | |
| .step-card.completed .step-num { | |
| color: #000; | |
| } | |
| .step-title { | |
| font-weight: 600; | |
| font-size: 14px; | |
| letter-spacing: 0.5px; | |
| } | |
| .badge { | |
| font-size: 10px; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .badge.success { background: #E8F5E9; color: #2E7D32; } | |
| .badge.processing { background: #FF5722; color: #FFF; } | |
| .badge.pending { background: #F5F5F5; color: #999; } | |
| .badge.accent { background: #E3F2FD; color: #1565C0; } | |
| .card-content { | |
| /* No extra padding - uses step-card's padding */ | |
| } | |
| .api-note { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: #999; | |
| margin-bottom: 8px; | |
| } | |
| .description { | |
| font-size: 12px; | |
| color: #666; | |
| line-height: 1.5; | |
| margin-bottom: 16px; | |
| } | |
| /* Action Section */ | |
| .action-section { | |
| margin-top: 16px; | |
| } | |
| .action-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 12px 24px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .action-btn.primary { | |
| background: #000; | |
| color: #FFF; | |
| } | |
| .action-btn.primary:hover:not(:disabled) { | |
| opacity: 0.8; | |
| } | |
| .action-btn.secondary { | |
| background: #F5F5F5; | |
| color: #333; | |
| } | |
| .action-btn.secondary:hover:not(:disabled) { | |
| background: #E5E5E5; | |
| } | |
| .action-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .action-group { | |
| display: flex; | |
| gap: 12px; | |
| margin-top: 16px; | |
| } | |
| .action-group.dual { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| .action-group.dual .action-btn { | |
| width: 100%; | |
| } | |
| /* Info Card */ | |
| .info-card { | |
| background: #F5F5F5; | |
| border-radius: 6px; | |
| padding: 16px; | |
| margin-top: 16px; | |
| } | |
| .info-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 8px 0; | |
| border-bottom: 1px dashed #E0E0E0; | |
| } | |
| .info-row:last-child { | |
| border-bottom: none; | |
| } | |
| .info-label { | |
| font-size: 12px; | |
| color: #666; | |
| } | |
| .info-value { | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| .info-value.mono { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| } | |
| /* Stats Grid */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr 1fr; | |
| gap: 12px; | |
| background: #F9F9F9; | |
| padding: 16px; | |
| border-radius: 6px; | |
| } | |
| .stat-card { | |
| text-align: center; | |
| } | |
| .stat-value { | |
| display: block; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #000; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .stat-label { | |
| font-size: 9px; | |
| color: #999; | |
| text-transform: uppercase; | |
| margin-top: 4px; | |
| display: block; | |
| } | |
| /* Profiles Preview */ | |
| .profiles-preview { | |
| margin-top: 20px; | |
| border-top: 1px solid #E5E5E5; | |
| padding-top: 16px; | |
| } | |
| .preview-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .preview-title { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #666; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .profiles-list { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| max-height: 320px; | |
| overflow-y: auto; | |
| padding-right: 4px; | |
| } | |
| .profiles-list::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .profiles-list::-webkit-scrollbar-thumb { | |
| background: #DDD; | |
| border-radius: 2px; | |
| } | |
| .profiles-list::-webkit-scrollbar-thumb:hover { | |
| background: #CCC; | |
| } | |
| .profile-card { | |
| background: #FAFAFA; | |
| border: 1px solid #E5E5E5; | |
| border-radius: 6px; | |
| padding: 14px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .profile-card:hover { | |
| border-color: #999; | |
| background: #FFF; | |
| } | |
| .profile-header { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 8px; | |
| margin-bottom: 6px; | |
| } | |
| .profile-realname { | |
| font-size: 14px; | |
| font-weight: 700; | |
| color: #000; | |
| } | |
| .profile-username { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: #999; | |
| } | |
| .profile-meta { | |
| margin-bottom: 8px; | |
| } | |
| .profile-profession { | |
| font-size: 11px; | |
| color: #666; | |
| background: #F0F0F0; | |
| padding: 2px 8px; | |
| border-radius: 3px; | |
| } | |
| .profile-bio { | |
| font-size: 12px; | |
| color: #444; | |
| line-height: 1.6; | |
| margin: 0 0 10px 0; | |
| display: -webkit-box; | |
| -webkit-line-clamp: 3; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| .profile-topics { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .topic-tag { | |
| font-size: 10px; | |
| color: #1565C0; | |
| background: #E3F2FD; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| } | |
| .topic-more { | |
| font-size: 10px; | |
| color: #999; | |
| padding: 2px 6px; | |
| } | |
| /* Config Preview */ | |
| /* Config Detail Panel */ | |
| .config-detail-panel { | |
| margin-top: 16px; | |
| } | |
| .config-block { | |
| margin-top: 16px; | |
| border-top: 1px solid #E5E5E5; | |
| padding-top: 12px; | |
| } | |
| .config-block:first-child { | |
| margin-top: 0; | |
| border-top: none; | |
| padding-top: 0; | |
| } | |
| .config-block-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .config-block-title { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #666; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .config-block-badge { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| background: #F1F5F9; | |
| color: #475569; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| } | |
| /* Config Grid */ | |
| .config-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| } | |
| .config-item { | |
| background: #F9F9F9; | |
| padding: 12px 14px; | |
| border-radius: 6px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .config-item-label { | |
| font-size: 11px; | |
| color: #94A3B8; | |
| } | |
| .config-item-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: #1E293B; | |
| } | |
| /* Time Periods */ | |
| .time-periods { | |
| margin-top: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .period-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 8px 12px; | |
| background: #F9F9F9; | |
| border-radius: 6px; | |
| } | |
| .period-label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #64748B; | |
| min-width: 70px; | |
| } | |
| .period-hours { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: #475569; | |
| flex: 1; | |
| } | |
| .period-multiplier { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: #6366F1; | |
| background: #EEF2FF; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| } | |
| /* Agents Cards */ | |
| .agents-cards { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| padding-right: 4px; | |
| } | |
| .agents-cards::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .agents-cards::-webkit-scrollbar-thumb { | |
| background: #DDD; | |
| border-radius: 2px; | |
| } | |
| .agents-cards::-webkit-scrollbar-thumb:hover { | |
| background: #CCC; | |
| } | |
| .agent-card { | |
| background: #F9F9F9; | |
| border: 1px solid #E5E5E5; | |
| border-radius: 6px; | |
| padding: 14px; | |
| transition: all 0.2s ease; | |
| } | |
| .agent-card:hover { | |
| border-color: #999; | |
| background: #FFF; | |
| } | |
| /* Agent Card Header */ | |
| .agent-card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 14px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid #F1F5F9; | |
| } | |
| .agent-identity { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .agent-id { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: #94A3B8; | |
| } | |
| .agent-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #1E293B; | |
| } | |
| .agent-tags { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .agent-type { | |
| font-size: 10px; | |
| color: #64748B; | |
| background: #F1F5F9; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| } | |
| .agent-stance { | |
| font-size: 10px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| } | |
| .stance-neutral { | |
| background: #F1F5F9; | |
| color: #64748B; | |
| } | |
| .stance-supportive { | |
| background: #DCFCE7; | |
| color: #16A34A; | |
| } | |
| .stance-opposing { | |
| background: #FEE2E2; | |
| color: #DC2626; | |
| } | |
| .stance-observer { | |
| background: #FEF3C7; | |
| color: #D97706; | |
| } | |
| /* Agent Timeline */ | |
| .agent-timeline { | |
| margin-bottom: 14px; | |
| } | |
| .timeline-label { | |
| display: block; | |
| font-size: 10px; | |
| color: #94A3B8; | |
| margin-bottom: 6px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .mini-timeline { | |
| display: flex; | |
| gap: 2px; | |
| height: 16px; | |
| background: #F8FAFC; | |
| border-radius: 4px; | |
| padding: 3px; | |
| } | |
| .timeline-hour { | |
| flex: 1; | |
| background: #E2E8F0; | |
| border-radius: 2px; | |
| transition: all 0.2s; | |
| } | |
| .timeline-hour.active { | |
| background: linear-gradient(180deg, #6366F1, #818CF8); | |
| } | |
| .timeline-marks { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 4px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 9px; | |
| color: #94A3B8; | |
| } | |
| /* Agent Params */ | |
| .agent-params { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .param-group { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| } | |
| .param-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .param-item .param-label { | |
| font-size: 10px; | |
| color: #94A3B8; | |
| } | |
| .param-item .param-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #475569; | |
| } | |
| .param-value.with-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .mini-bar { | |
| height: 4px; | |
| background: linear-gradient(90deg, #6366F1, #A855F7); | |
| border-radius: 2px; | |
| min-width: 4px; | |
| max-width: 40px; | |
| } | |
| .param-value.positive { | |
| color: #16A34A; | |
| } | |
| .param-value.negative { | |
| color: #DC2626; | |
| } | |
| .param-value.neutral { | |
| color: #64748B; | |
| } | |
| .param-value.highlight { | |
| color: #6366F1; | |
| } | |
| /* Platforms Grid */ | |
| .platforms-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| } | |
| .platform-card { | |
| background: #F9F9F9; | |
| padding: 14px; | |
| border-radius: 6px; | |
| } | |
| .platform-card-header { | |
| margin-bottom: 10px; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid #E5E5E5; | |
| } | |
| .platform-name { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .platform-params { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .param-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .param-label { | |
| font-size: 12px; | |
| color: #64748B; | |
| } | |
| .param-value { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #1E293B; | |
| } | |
| /* Reasoning Content */ | |
| .reasoning-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .reasoning-item { | |
| padding: 12px 14px; | |
| background: #F9F9F9; | |
| border-radius: 6px; | |
| } | |
| .reasoning-text { | |
| font-size: 13px; | |
| color: #555; | |
| line-height: 1.7; | |
| margin: 0; | |
| } | |
| /* Profile Modal */ | |
| .profile-modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.6); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| backdrop-filter: blur(4px); | |
| } | |
| .profile-modal { | |
| background: #FFF; | |
| border-radius: 16px; | |
| width: 90%; | |
| max-width: 600px; | |
| max-height: 85vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| padding: 24px; | |
| background: #FFF; | |
| border-bottom: 1px solid #F0F0F0; | |
| } | |
| .modal-header-info { | |
| flex: 1; | |
| } | |
| .modal-name-row { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .modal-realname { | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #000; | |
| } | |
| .modal-username { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 13px; | |
| color: #999; | |
| } | |
| .modal-profession { | |
| font-size: 12px; | |
| color: #666; | |
| background: #F5F5F5; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| display: inline-block; | |
| font-weight: 500; | |
| } | |
| .close-btn { | |
| width: 32px; | |
| height: 32px; | |
| border: none; | |
| background: none; | |
| color: #999; | |
| border-radius: 50%; | |
| font-size: 24px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| line-height: 1; | |
| transition: color 0.2s; | |
| padding: 0; | |
| } | |
| .close-btn:hover { | |
| color: #333; | |
| } | |
| .modal-body { | |
| padding: 24px; | |
| overflow-y: auto; | |
| flex: 1; | |
| } | |
| /* 基本信息网格 */ | |
| .modal-info-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 24px 16px; | |
| margin-bottom: 32px; | |
| padding: 0; | |
| background: transparent; | |
| border-radius: 0; | |
| } | |
| .info-item { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .info-label { | |
| font-size: 11px; | |
| color: #999; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| font-weight: 600; | |
| } | |
| .info-value { | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .info-value.mbti { | |
| font-family: 'JetBrains Mono', monospace; | |
| color: #FF5722; | |
| } | |
| /* 模块区域 */ | |
| .modal-section { | |
| margin-bottom: 28px; | |
| } | |
| .section-label { | |
| display: block; | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: #999; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 12px; | |
| } | |
| .section-bio { | |
| font-size: 14px; | |
| color: #333; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 16px; | |
| background: #F9F9F9; | |
| border-radius: 6px; | |
| border-left: 3px solid #E0E0E0; | |
| } | |
| /* 话题标签 */ | |
| .topics-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .topic-item { | |
| font-size: 11px; | |
| color: #1565C0; | |
| background: #E3F2FD; | |
| padding: 4px 10px; | |
| border-radius: 12px; | |
| transition: all 0.2s; | |
| border: none; | |
| } | |
| .topic-item:hover { | |
| background: #BBDEFB; | |
| color: #0D47A1; | |
| } | |
| /* 详细人设 */ | |
| .persona-dimensions { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 12px; | |
| margin-bottom: 16px; | |
| } | |
| .dimension-card { | |
| background: #F8F9FA; | |
| padding: 12px; | |
| border-radius: 6px; | |
| border-left: 3px solid #DDD; | |
| transition: all 0.2s; | |
| } | |
| .dimension-card:hover { | |
| background: #F0F0F0; | |
| border-left-color: #999; | |
| } | |
| .dim-title { | |
| display: block; | |
| font-size: 12px; | |
| font-weight: 700; | |
| color: #333; | |
| margin-bottom: 4px; | |
| } | |
| .dim-desc { | |
| display: block; | |
| font-size: 10px; | |
| color: #888; | |
| line-height: 1.4; | |
| } | |
| .persona-content { | |
| max-height: none; | |
| overflow: visible; | |
| padding: 0; | |
| background: transparent; | |
| border: none; | |
| border-radius: 0; | |
| } | |
| .persona-content::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .persona-content::-webkit-scrollbar-thumb { | |
| background: #DDD; | |
| border-radius: 2px; | |
| } | |
| .section-persona { | |
| font-size: 13px; | |
| color: #555; | |
| line-height: 1.8; | |
| margin: 0; | |
| text-align: justify; | |
| } | |
| /* System Logs */ | |
| .system-logs { | |
| background: #000; | |
| color: #DDD; | |
| padding: 16px; | |
| font-family: 'JetBrains Mono', monospace; | |
| border-top: 1px solid #222; | |
| flex-shrink: 0; | |
| } | |
| .log-header { | |
| display: flex; | |
| justify-content: space-between; | |
| border-bottom: 1px solid #333; | |
| padding-bottom: 8px; | |
| margin-bottom: 8px; | |
| font-size: 10px; | |
| color: #888; | |
| } | |
| .log-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| height: 80px; /* Approx 4 lines visible */ | |
| overflow-y: auto; | |
| padding-right: 4px; | |
| } | |
| .log-content::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .log-content::-webkit-scrollbar-thumb { | |
| background: #333; | |
| border-radius: 2px; | |
| } | |
| .log-line { | |
| font-size: 11px; | |
| display: flex; | |
| gap: 12px; | |
| line-height: 1.5; | |
| } | |
| .log-time { | |
| color: #666; | |
| min-width: 75px; | |
| } | |
| .log-msg { | |
| color: #CCC; | |
| word-break: break-all; | |
| } | |
| /* Spinner */ | |
| .spinner-sm { | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid #E5E5E5; | |
| border-top-color: #FF5722; | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Orchestration Content */ | |
| .orchestration-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| margin-top: 16px; | |
| } | |
| .box-label { | |
| display: block; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #666; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 12px; | |
| } | |
| .narrative-box { | |
| background: #FFFFFF; | |
| padding: 20px 24px; | |
| border-radius: 12px; | |
| border: 1px solid #EEF2F6; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.03); | |
| transition: all 0.3s ease; | |
| } | |
| .narrative-box .box-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| color: #666; | |
| font-size: 13px; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| } | |
| .special-icon { | |
| filter: drop-shadow(0 2px 4px rgba(255, 87, 34, 0.2)); | |
| transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| } | |
| .narrative-box:hover .special-icon { | |
| transform: rotate(180deg); | |
| } | |
| .narrative-text { | |
| font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif; | |
| font-size: 14px; | |
| color: #334155; | |
| line-height: 1.8; | |
| margin: 0; | |
| text-align: justify; | |
| letter-spacing: 0.01em; | |
| } | |
| .topics-section { | |
| background: #FFF; | |
| } | |
| .hot-topics-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .hot-topic-tag { | |
| font-size: 12px; | |
| color:rgba(255, 86, 34, 0.88); | |
| background: #FFF3E0; | |
| padding: 4px 10px; | |
| border-radius: 12px; | |
| font-weight: 500; | |
| } | |
| .hot-topic-more { | |
| font-size: 11px; | |
| color: #999; | |
| padding: 4px 6px; | |
| } | |
| .initial-posts-section { | |
| border-top: 1px solid #EAEAEA; | |
| padding-top: 16px; | |
| } | |
| .posts-timeline { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| padding-left: 8px; | |
| border-left: 2px solid #F0F0F0; | |
| margin-top: 12px; | |
| } | |
| .timeline-item { | |
| position: relative; | |
| padding-left: 20px; | |
| } | |
| .timeline-marker { | |
| position: absolute; | |
| left: 0; | |
| top: 14px; | |
| width: 12px; | |
| height: 2px; | |
| background: #DDD; | |
| } | |
| .timeline-content { | |
| background: #F9F9F9; | |
| padding: 12px; | |
| border-radius: 6px; | |
| border: 1px solid #EEE; | |
| } | |
| .post-header { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 6px; | |
| } | |
| .post-role { | |
| font-size: 11px; | |
| font-weight: 700; | |
| color: #333; | |
| text-transform: uppercase; | |
| } | |
| .post-agent-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .post-id, | |
| .post-username { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: #666; | |
| line-height: 1; | |
| vertical-align: baseline; | |
| } | |
| .post-username { | |
| margin-right: 6px; | |
| } | |
| .post-text { | |
| font-size: 12px; | |
| color: #555; | |
| line-height: 1.5; | |
| margin: 0; | |
| } | |
| /* 模拟轮数配置样式 */ | |
| .rounds-config-section { | |
| margin: 24px 0; | |
| padding-top: 24px; | |
| border-top: 1px solid #EAEAEA; | |
| } | |
| .rounds-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .header-left { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .section-title { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #1E293B; | |
| } | |
| .section-desc { | |
| font-size: 12px; | |
| color: #94A3B8; | |
| } | |
| .desc-highlight { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-weight: 600; | |
| color: #1E293B; | |
| background: #F1F5F9; | |
| padding: 1px 6px; | |
| border-radius: 4px; | |
| margin: 0 2px; | |
| } | |
| /* Switch Control */ | |
| .switch-control { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| cursor: pointer; | |
| padding: 4px 8px 4px 4px; | |
| border-radius: 20px; | |
| transition: background 0.2s; | |
| } | |
| .switch-control:hover { | |
| background: #F8FAFC; | |
| } | |
| .switch-control input { | |
| display: none; | |
| } | |
| .switch-track { | |
| width: 36px; | |
| height: 20px; | |
| background: #E2E8F0; | |
| border-radius: 10px; | |
| position: relative; | |
| transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); | |
| } | |
| .switch-track::after { | |
| content: ''; | |
| position: absolute; | |
| left: 2px; | |
| top: 2px; | |
| width: 16px; | |
| height: 16px; | |
| background: #FFF; | |
| border-radius: 50%; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); | |
| } | |
| .switch-control input:checked + .switch-track { | |
| background: #000; | |
| } | |
| .switch-control input:checked + .switch-track::after { | |
| transform: translateX(16px); | |
| } | |
| .switch-label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #64748B; | |
| } | |
| .switch-control input:checked ~ .switch-label { | |
| color: #1E293B; | |
| } | |
| /* Slider Content */ | |
| .rounds-content { | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .slider-display { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-end; | |
| margin-bottom: 16px; | |
| } | |
| .slider-main-value { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 4px; | |
| } | |
| .val-num { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 24px; | |
| font-weight: 700; | |
| color: #000; | |
| } | |
| .val-unit { | |
| font-size: 12px; | |
| color: #666; | |
| font-weight: 500; | |
| } | |
| .slider-meta-info { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| color: #64748B; | |
| background: #F1F5F9; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| } | |
| .range-wrapper { | |
| position: relative; | |
| padding: 0 2px; | |
| } | |
| .minimal-slider { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: #E2E8F0; | |
| border-radius: 2px; | |
| outline: none; | |
| background-image: linear-gradient(#000, #000); | |
| background-size: var(--percent, 0%) 100%; | |
| background-repeat: no-repeat; | |
| cursor: pointer; | |
| } | |
| .minimal-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: #FFF; | |
| border: 2px solid #000; | |
| cursor: pointer; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.1); | |
| transition: transform 0.1s; | |
| margin-top: -6px; /* Center thumb */ | |
| } | |
| .minimal-slider::-webkit-slider-thumb:hover { | |
| transform: scale(1.1); | |
| } | |
| .minimal-slider::-webkit-slider-runnable-track { | |
| height: 4px; | |
| border-radius: 2px; | |
| } | |
| .range-marks { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 8px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: #94A3B8; | |
| position: relative; | |
| } | |
| .mark-recommend { | |
| cursor: pointer; | |
| transition: color 0.2s; | |
| position: relative; | |
| } | |
| .mark-recommend:hover { | |
| color: #000; | |
| } | |
| .mark-recommend.active { | |
| color: #000; | |
| font-weight: 600; | |
| } | |
| .mark-recommend::after { | |
| content: ''; | |
| position: absolute; | |
| top: -12px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 1px; | |
| height: 4px; | |
| background: #CBD5E1; | |
| } | |
| /* Auto Info */ | |
| .auto-info-card { | |
| display: flex; | |
| align-items: center; | |
| gap: 24px; | |
| background: #F8FAFC; | |
| padding: 16px 20px; | |
| border-radius: 8px; | |
| } | |
| .auto-value { | |
| display: flex; | |
| flex-direction: row; | |
| align-items: baseline; | |
| gap: 4px; | |
| padding-right: 24px; | |
| border-right: 1px solid #E2E8F0; | |
| } | |
| .auto-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| justify-content: center; | |
| } | |
| .auto-meta-row { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .duration-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 11px; | |
| font-weight: 500; | |
| color: #64748B; | |
| background: #FFFFFF; | |
| border: 1px solid #E2E8F0; | |
| padding: 3px 8px; | |
| border-radius: 6px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.02); | |
| } | |
| .auto-desc { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .auto-desc p { | |
| margin: 0; | |
| font-size: 13px; | |
| color: #64748B; | |
| line-height: 1.5; | |
| } | |
| .highlight-tip { | |
| margin-top: 4px ; | |
| font-size: 12px ; | |
| color: #000 ; | |
| font-weight: 500; | |
| cursor: pointer; | |
| } | |
| .highlight-tip:hover { | |
| text-decoration: underline; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(4px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .fade-enter-active, | |
| .fade-leave-active { | |
| transition: opacity 0.2s ease; | |
| } | |
| .fade-enter-from, | |
| .fade-leave-to { | |
| opacity: 0; | |
| } | |
| /* Modal Transition */ | |
| .modal-enter-active, | |
| .modal-leave-active { | |
| transition: opacity 0.3s ease; | |
| } | |
| .modal-enter-from, | |
| .modal-leave-to { | |
| opacity: 0; | |
| } | |
| .modal-enter-active .profile-modal { | |
| transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| } | |
| .modal-leave-active .profile-modal { | |
| transition: all 0.3s ease-in; | |
| } | |
| .modal-enter-from .profile-modal, | |
| .modal-leave-to .profile-modal { | |
| transform: scale(0.95) translateY(10px); | |
| opacity: 0; | |
| } | |
| </style> | |