| <template> |
| <div class="env-setup-panel"> |
| <div class="scroll-container"> |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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']) |
| |
| |
| const phase = ref(0) |
| 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) |
| |
| |
| 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 |
| } |
| const totalHours = simulationConfig.value.time_config.total_simulation_hours |
| const minutesPerRound = simulationConfig.value.time_config.minutes_per_round |
| if (!totalHours || !minutesPerRound) { |
| return null |
| } |
| const calculatedRounds = Math.floor((totalHours * 60) / minutesPerRound) |
| |
| return Math.max(calculatedRounds, 40) |
| }) |
| |
| |
| let pollTimer = null |
| let profilesTimer = null |
| let configTimer = null |
| |
| |
| const displayProfiles = computed(() => { |
| if (showProfilesDetail.value) { |
| return profiles.value |
| } |
| return profiles.value.slice(0, 6) |
| }) |
| |
| |
| 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) |
| }) |
| |
| |
| const addLog = (msg) => { |
| emit('add-log', msg) |
| } |
| |
| |
| const handleStartSimulation = () => { |
| |
| const params = {} |
| |
| if (useCustomRounds.value) { |
| |
| params.maxRounds = customMaxRounds.value |
| addLog(`开始模拟,自定义轮数: ${customMaxRounds.value} 轮`) |
| } else { |
| |
| 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}`) |
| |
| |
| 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() |
| |
| 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 || [] |
| |
| 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) |
| |
| |
| 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('正在加载已有配置数据...') |
| |
| |
| 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') |
| } |
| } |
| |
| |
| 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 { |
| 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 { |
| |
| } |
| |
| .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 { |
| 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 { |
| 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 { |
| 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 { |
| 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-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 { |
| 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 { |
| 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 { |
| 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 { |
| 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 { |
| 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 { |
| 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 { |
| 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 { |
| 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-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 { |
| 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; |
| 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-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 { |
| 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 { |
| 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; |
| } |
| |
| |
| .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; |
| } |
| |
| .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-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 !important; |
| font-size: 12px !important; |
| color: #000 !important; |
| 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-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> |
| |