2590 lines
68 KiB
Vue
2590 lines
68 KiB
Vue
<template>
|
||
<div class="env-setup-panel">
|
||
<div class="scroll-container">
|
||
<!-- Step 01: Simulation instance -->
|
||
<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">{{ $t('step2.simInstanceInit') }}</span>
|
||
</div>
|
||
<div class="step-status">
|
||
<span v-if="phase > 0" class="badge success">{{ $t('common.completed') }}</span>
|
||
<span v-else class="badge processing">{{ $t('step2.initializing') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-content">
|
||
<p class="api-note">POST /api/simulation/create</p>
|
||
<p class="description">
|
||
{{ $t('step2.simInstanceDesc') }}
|
||
</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 || $t('step2.asyncTaskDone') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 02: Generate agent personas -->
|
||
<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">{{ $t('step2.generateAgentPersona') }}</span>
|
||
</div>
|
||
<div class="step-status">
|
||
<span v-if="phase > 1" class="badge success">{{ $t('common.completed') }}</span>
|
||
<span v-else-if="phase === 1" class="badge processing">{{ prepareProgress }}%</span>
|
||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-content">
|
||
<p class="api-note">POST /api/simulation/prepare</p>
|
||
<p class="description">
|
||
{{ $t('step2.generateAgentPersonaDesc') }}
|
||
</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">{{ $t('step2.currentAgentCount') }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-value">{{ expectedTotal || '-' }}</span>
|
||
<span class="stat-label">{{ $t('step2.expectedAgentTotal') }}</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-value">{{ totalTopicsCount }}</span>
|
||
<span class="stat-label">{{ $t('step2.relatedTopicsCount') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Profiles List Preview -->
|
||
<div v-if="profiles.length > 0" class="profiles-preview">
|
||
<div class="preview-header">
|
||
<span class="preview-title">{{ $t('step2.generatedAgentPersonas') }}</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 || $t('step2.unknownProfession') }}</span>
|
||
</div>
|
||
<p class="profile-bio">{{ profile.bio || $t('step2.noBio') }}</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: Generate dual-platform simulation config -->
|
||
<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">{{ $t('step2.dualPlatformConfig') }}</span>
|
||
</div>
|
||
<div class="step-status">
|
||
<span v-if="phase > 2" class="badge success">{{ $t('common.completed') }}</span>
|
||
<span v-else-if="phase === 2" class="badge processing">{{ $t('step2.generating') }}</span>
|
||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-content">
|
||
<p class="api-note">POST /api/simulation/prepare</p>
|
||
<p class="description">
|
||
{{ $t('step2.dualPlatformConfigDesc') }}
|
||
</p>
|
||
|
||
<!-- Config Preview -->
|
||
<div v-if="simulationConfig" class="config-detail-panel">
|
||
<!-- Time config -->
|
||
<div class="config-block">
|
||
<div class="config-grid">
|
||
<div class="config-item">
|
||
<span class="config-item-label">{{ $t('step2.simulationDuration') }}</span>
|
||
<span class="config-item-value">{{ simulationConfig.time_config?.total_simulation_hours || '-' }} {{ $t('common.hours') }}</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="config-item-label">{{ $t('step2.roundDuration') }}</span>
|
||
<span class="config-item-value">{{ simulationConfig.time_config?.minutes_per_round || '-' }} {{ $t('common.minutes') }}</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="config-item-label">{{ $t('step2.totalRounds') }}</span>
|
||
<span class="config-item-value">{{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} {{ $t('common.rounds') }}</span>
|
||
</div>
|
||
<div class="config-item">
|
||
<span class="config-item-label">{{ $t('step2.activePerHour') }}</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">{{ $t('step2.peakHours') }}</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">{{ $t('step2.workHours') }}</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">{{ $t('step2.morningHours') }}</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">{{ $t('step2.offPeakHours') }}</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 config -->
|
||
<div class="config-block">
|
||
<div class="config-block-header">
|
||
<span class="config-block-title">{{ $t('step2.agentConfig') }}</span>
|
||
<span class="config-block-badge">{{ simulationConfig.agent_configs?.length || 0 }} {{ $t('common.items') }}</span>
|
||
</div>
|
||
<div class="agents-cards">
|
||
<div
|
||
v-for="agent in simulationConfig.agent_configs"
|
||
:key="agent.agent_id"
|
||
class="agent-card"
|
||
>
|
||
<!-- Card header -->
|
||
<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>
|
||
|
||
<!-- Active-hours timeline -->
|
||
<div class="agent-timeline">
|
||
<span class="timeline-label">{{ $t('step2.activeTimePeriod') }}</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>
|
||
|
||
<!-- Behavior params -->
|
||
<div class="agent-params">
|
||
<div class="param-group">
|
||
<div class="param-item">
|
||
<span class="param-label">{{ $t('step2.postsPerHour') }}</span>
|
||
<span class="param-value">{{ agent.posts_per_hour }}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">{{ $t('step2.commentsPerHour') }}</span>
|
||
<span class="param-value">{{ agent.comments_per_hour }}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">{{ $t('step2.responseDelay') }}</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">{{ $t('step2.activityLevel') }}</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">{{ $t('step2.sentimentBias') }}</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">{{ $t('step2.influenceWeight') }}</span>
|
||
<span class="param-value highlight">{{ agent.influence_weight?.toFixed(1) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Platform config -->
|
||
<div class="config-block">
|
||
<div class="config-block-header">
|
||
<span class="config-block-title">{{ $t('step2.recommendAlgoConfig') }}</span>
|
||
</div>
|
||
<div class="platforms-grid">
|
||
<div v-if="simulationConfig.twitter_config" class="platform-card">
|
||
<div class="platform-card-header">
|
||
<span class="platform-name">{{ $t('step2.platform1Name') }}</span>
|
||
</div>
|
||
<div class="platform-params">
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.recencyWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.twitter_config.recency_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.popularityWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.twitter_config.popularity_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.relevanceWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.twitter_config.relevance_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.viralThreshold') }}</span>
|
||
<span class="param-value">{{ simulationConfig.twitter_config.viral_threshold }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.echoChamberStrength') }}</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">{{ $t('step2.platform2Name') }}</span>
|
||
</div>
|
||
<div class="platform-params">
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.recencyWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.reddit_config.recency_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.popularityWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.reddit_config.popularity_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.relevanceWeight') }}</span>
|
||
<span class="param-value">{{ simulationConfig.reddit_config.relevance_weight }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.viralThreshold') }}</span>
|
||
<span class="param-value">{{ simulationConfig.reddit_config.viral_threshold }}</span>
|
||
</div>
|
||
<div class="param-row">
|
||
<span class="param-label">{{ $t('step2.echoChamberStrength') }}</span>
|
||
<span class="param-value">{{ simulationConfig.reddit_config.echo_chamber_strength }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LLM config reasoning -->
|
||
<div v-if="simulationConfig.generation_reasoning" class="config-block">
|
||
<div class="config-block-header">
|
||
<span class="config-block-title">{{ $t('step2.llmConfigReasoning') }}</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: Initial activation orchestration -->
|
||
<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">{{ $t('step2.initialActivation') }}</span>
|
||
</div>
|
||
<div class="step-status">
|
||
<span v-if="phase > 3" class="badge success">{{ $t('common.completed') }}</span>
|
||
<span v-else-if="phase === 3" class="badge processing">{{ $t('step2.orchestrating') }}</span>
|
||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-content">
|
||
<p class="api-note">POST /api/simulation/prepare</p>
|
||
<p class="description">
|
||
{{ $t('step2.initialActivationDesc') }}
|
||
</p>
|
||
|
||
<div v-if="simulationConfig?.event_config" class="orchestration-content">
|
||
<!-- Narrative direction -->
|
||
<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>
|
||
{{ $t('step2.narrativeDirection') }}
|
||
</span>
|
||
<p class="narrative-text">{{ simulationConfig.event_config.narrative_direction }}</p>
|
||
</div>
|
||
|
||
<!-- Hot topics -->
|
||
<div class="topics-section">
|
||
<span class="box-label">{{ $t('step2.initialHotTopics') }}</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>
|
||
|
||
<!-- Initial post timeline -->
|
||
<div class="initial-posts-section">
|
||
<span class="box-label">{{ $t('step2.initialActivationSeq', { count: 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: Setup complete -->
|
||
<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">{{ $t('step2.setupComplete') }}</span>
|
||
</div>
|
||
<div class="step-status">
|
||
<span v-if="phase >= 4" class="badge processing">{{ $t('step1.inProgress') }}</span>
|
||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-content">
|
||
<p class="api-note">POST /api/simulation/start</p>
|
||
<p class="description">{{ $t('step2.setupCompleteDesc') }}</p>
|
||
|
||
<!-- Round-count config: only render once the config and round count are ready -->
|
||
<div v-if="simulationConfig && autoGeneratedRounds" class="rounds-config-section">
|
||
<div class="rounds-header">
|
||
<div class="header-left">
|
||
<span class="section-title">{{ $t('step2.roundsConfig') }}</span>
|
||
<span class="section-desc">{{ $t('step2.roundsConfigDesc', { hours: simulationConfig?.time_config?.total_simulation_hours || '-', minutesPerRound: simulationConfig?.time_config?.minutes_per_round || '-' }) }}</span>
|
||
</div>
|
||
<label class="switch-control">
|
||
<input type="checkbox" v-model="useCustomRounds">
|
||
<span class="switch-track"></span>
|
||
<span class="switch-label">{{ $t('step2.customToggle') }}</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">{{ $t('step2.roundsUnit') }}</span>
|
||
</div>
|
||
<div class="slider-meta-info">
|
||
<span>{{ $t('step2.estimatedDuration', { minutes: 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)` }"
|
||
>{{ $t('step2.recommendedRounds', { rounds: 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">{{ $t('step2.roundsUnit') }}</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>
|
||
{{ $t('step2.estimatedDurationFull', { minutes: Math.round(autoGeneratedRounds * 0.6) }) }}
|
||
</span>
|
||
</div>
|
||
<div class="auto-desc">
|
||
<p class="highlight-tip" @click="useCustomRounds = true">{{ $t('step2.customTip') }} ➝</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
|
||
<div class="action-group dual">
|
||
<button
|
||
class="action-btn secondary"
|
||
@click="$emit('go-back')"
|
||
>
|
||
← {{ $t('step2.backToGraphBuild') }}
|
||
</button>
|
||
<button
|
||
class="action-btn primary"
|
||
:disabled="phase < 4"
|
||
@click="handleStartSimulation"
|
||
>
|
||
{{ $t('step2.startDualWorldSim') }} ➝
|
||
</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">
|
||
<!-- Basic info -->
|
||
<div class="modal-info-grid">
|
||
<div class="info-item">
|
||
<span class="info-label">{{ $t('step2.profileModalAge') }}</span>
|
||
<span class="info-value">{{ selectedProfile.age || '-' }} {{ $t('step2.yearsOld') }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">{{ $t('step2.profileModalGender') }}</span>
|
||
<span class="info-value">{{ { male: $t('step2.genderMale'), female: $t('step2.genderFemale'), other: $t('step2.genderOther') }[selectedProfile.gender] || selectedProfile.gender }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">{{ $t('step2.profileModalCountry') }}</span>
|
||
<span class="info-value">{{ selectedProfile.country || '-' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">{{ $t('step2.profileModalMbti') }}</span>
|
||
<span class="info-value mbti">{{ selectedProfile.mbti || '-' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bio -->
|
||
<div class="modal-section">
|
||
<span class="section-label">{{ $t('step2.profileModalBio') }}</span>
|
||
<p class="section-bio">{{ selectedProfile.bio || $t('step2.noBio') }}</p>
|
||
</div>
|
||
|
||
<!-- Followed topics -->
|
||
<div class="modal-section" v-if="selectedProfile.interested_topics?.length">
|
||
<span class="section-label">{{ $t('step2.profileModalTopics') }}</span>
|
||
<div class="topics-grid">
|
||
<span
|
||
v-for="topic in selectedProfile.interested_topics"
|
||
:key="topic"
|
||
class="topic-item"
|
||
>{{ topic }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detailed persona -->
|
||
<div class="modal-section" v-if="selectedProfile.persona">
|
||
<span class="section-label">{{ $t('step2.profileModalPersona') }}</span>
|
||
|
||
<!-- Persona dimensions overview -->
|
||
<div class="persona-dimensions">
|
||
<div class="dimension-card">
|
||
<span class="dim-title">{{ $t('step2.personaDimExperience') }}</span>
|
||
<span class="dim-desc">{{ $t('step2.personaDimExperienceDesc') }}</span>
|
||
</div>
|
||
<div class="dimension-card">
|
||
<span class="dim-title">{{ $t('step2.personaDimBehavior') }}</span>
|
||
<span class="dim-desc">{{ $t('step2.personaDimBehaviorDesc') }}</span>
|
||
</div>
|
||
<div class="dimension-card">
|
||
<span class="dim-title">{{ $t('step2.personaDimMemory') }}</span>
|
||
<span class="dim-desc">{{ $t('step2.personaDimMemoryDesc') }}</span>
|
||
</div>
|
||
<div class="dimension-card">
|
||
<span class="dim-title">{{ $t('step2.personaDimSocial') }}</span>
|
||
<span class="dim-desc">{{ $t('step2.personaDimSocialDesc') }}</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 { useI18n } from 'vue-i18n'
|
||
import {
|
||
prepareSimulation,
|
||
getPrepareStatus,
|
||
getSimulationProfilesRealtime,
|
||
getSimulationConfig,
|
||
getSimulationConfigRealtime
|
||
} from '../api/simulation'
|
||
|
||
const { t } = useI18n()
|
||
|
||
const props = defineProps({
|
||
simulationId: String, // Provided by the parent.
|
||
projectData: Object,
|
||
graphData: Object,
|
||
systemLogs: Array
|
||
})
|
||
|
||
const emit = defineEmits(['go-back', 'next-step', 'add-log', 'update-status'])
|
||
|
||
// State
|
||
const phase = ref(0) // 0: init, 1: generating personas, 2: generating config, 3: done
|
||
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)
|
||
|
||
// Log deduplication — remember the last emitted key so we don't repeat lines.
|
||
let lastLoggedMessage = ''
|
||
let lastLoggedProfileCount = 0
|
||
let lastLoggedConfigStage = ''
|
||
|
||
// Round-count configuration
|
||
const useCustomRounds = ref(false) // Default: use the auto-derived round count.
|
||
const customMaxRounds = ref(40) // Default recommendation: 40 rounds.
|
||
|
||
// 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
|
||
// Entering the config-generation stage — start polling the config endpoint.
|
||
if (!configTimer) {
|
||
addLog(t('log.startGeneratingConfig'))
|
||
startConfigPolling()
|
||
}
|
||
} else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') {
|
||
phase.value = 2 // Still part of the config stage.
|
||
}
|
||
})
|
||
|
||
// Compute the auto-derived round count from the simulation config (no hardcoded fallback).
|
||
const autoGeneratedRounds = computed(() => {
|
||
if (!simulationConfig.value?.time_config) {
|
||
return null // Config not generated yet.
|
||
}
|
||
const totalHours = simulationConfig.value.time_config.total_simulation_hours
|
||
const minutesPerRound = simulationConfig.value.time_config.minutes_per_round
|
||
if (!totalHours || !minutesPerRound) {
|
||
return null // Config data is incomplete.
|
||
}
|
||
const calculatedRounds = Math.floor((totalHours * 60) / minutesPerRound)
|
||
// Floor at 40 (the recommended baseline) so the slider range stays sane.
|
||
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)
|
||
})
|
||
|
||
// Look up the username for an agent_id from the profiles list.
|
||
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}`
|
||
}
|
||
|
||
// Total followed-topics count across all profiles.
|
||
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) {
|
||
// User chose a custom round count — pass max_rounds to the parent.
|
||
params.maxRounds = customMaxRounds.value
|
||
addLog(t('log.startSimCustomRounds', { rounds: customMaxRounds.value }))
|
||
} else {
|
||
// Keep the auto-derived round count — do not pass max_rounds.
|
||
addLog(t('log.startSimAutoRounds', { rounds: 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(t('log.errorMissingSimId'))
|
||
emit('update-status', 'error')
|
||
return
|
||
}
|
||
|
||
// Mark Step 1 done and move on to Step 2.
|
||
phase.value = 1
|
||
addLog(t('log.simInstanceCreated', { id: props.simulationId }))
|
||
addLog(t('log.preparingSimEnv'))
|
||
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(t('log.detectedExistingPrep'))
|
||
await loadPreparedData()
|
||
return
|
||
}
|
||
|
||
taskId.value = res.data.task_id
|
||
addLog(t('log.prepareTaskStarted'))
|
||
addLog(t('log.prepareTaskId', { taskId: res.data.task_id }))
|
||
|
||
// Pull the expected agent total straight from the prepare response.
|
||
if (res.data.expected_entities_count) {
|
||
expectedTotal.value = res.data.expected_entities_count
|
||
addLog(t('log.zepEntitiesFound', { count: res.data.expected_entities_count }))
|
||
if (res.data.entity_types && res.data.entity_types.length > 0) {
|
||
addLog(t('log.entityTypes', { types: res.data.entity_types.join(', ') }))
|
||
}
|
||
}
|
||
|
||
addLog(t('log.startPollingProgress'))
|
||
startPolling()
|
||
startProfilesPolling()
|
||
} else {
|
||
addLog(t('log.prepareFailed', { error: res.error || t('common.unknownError') }))
|
||
emit('update-status', 'error')
|
||
}
|
||
} catch (err) {
|
||
addLog(t('log.prepareException', { error: 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 || ''
|
||
|
||
// Parse the progress detail and emit one log line per change.
|
||
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) {
|
||
// Extract the stage label from the freeform 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(t('log.prepareComplete'))
|
||
stopPolling()
|
||
stopProfilesPolling()
|
||
await loadPreparedData()
|
||
} else if (data.status === 'failed') {
|
||
addLog(t('log.prepareFailedWithError', { error: data.error || t('common.unknownError') }))
|
||
stopPolling()
|
||
stopProfilesPolling()
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Failed to poll prepare status:', 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 || []
|
||
// Only overwrite the expected total when the API returns a non-zero value,
|
||
// so we don't clobber an already-known good value with a transient empty.
|
||
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)
|
||
|
||
// Log profile-generation progress only when the count changes.
|
||
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(t('log.startGeneratingAgentProfiles'))
|
||
}
|
||
addLog(t('log.agentProfile', { current: currentCount, total: total, name: profileName, profession: latestProfile?.profession || t('step2.unknownProfession') }))
|
||
|
||
if (expectedTotal.value && currentCount >= expectedTotal.value) {
|
||
addLog(t('log.allProfilesComplete', { count: currentCount }))
|
||
}
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Failed to fetch profiles:', err)
|
||
}
|
||
}
|
||
|
||
// Config polling
|
||
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
|
||
|
||
// Emit one log line per change of generation_stage.
|
||
if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) {
|
||
lastLoggedConfigStage = data.generation_stage
|
||
if (data.generation_stage === 'generating_profiles') {
|
||
addLog(t('log.generatingAgentProfileConfig'))
|
||
} else if (data.generation_stage === 'generating_config') {
|
||
addLog(t('log.generatingLLMConfig'))
|
||
}
|
||
}
|
||
|
||
if (data.config_generated && data.config) {
|
||
simulationConfig.value = data.config
|
||
addLog(t('log.configComplete'))
|
||
|
||
if (data.summary) {
|
||
addLog(t('log.configSummaryAgents', { count: data.summary.total_agents }))
|
||
addLog(t('log.configSummaryHours', { hours: data.summary.simulation_hours }))
|
||
addLog(t('log.configSummaryPosts', { count: data.summary.initial_posts_count }))
|
||
addLog(t('log.configSummaryTopics', { count: data.summary.hot_topics_count }))
|
||
addLog(t('log.configSummaryPlatforms', { twitter: data.summary.has_twitter_config ? '✓' : '✗', reddit: data.summary.has_reddit_config ? '✓' : '✗' }))
|
||
}
|
||
|
||
if (data.config.time_config) {
|
||
const tc = data.config.time_config
|
||
addLog(t('log.timeConfigDetail', { minutes: tc.minutes_per_round, rounds: 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(t('log.narrativeDirection', { direction: narrative.length > 50 ? narrative.substring(0, 50) + '...' : narrative }))
|
||
}
|
||
|
||
stopConfigPolling()
|
||
phase.value = 4
|
||
addLog(t('log.envSetupComplete'))
|
||
emit('update-status', 'completed')
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('Failed to fetch config:', err)
|
||
}
|
||
}
|
||
|
||
const loadPreparedData = async () => {
|
||
phase.value = 2
|
||
addLog(t('log.loadingExistingConfig'))
|
||
|
||
// Pull profiles one final time.
|
||
await fetchProfilesRealtime()
|
||
addLog(t('log.loadedAgentProfiles', { count: profiles.value.length }))
|
||
|
||
// Fetch the config via the realtime endpoint.
|
||
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(t('log.configLoadSuccess'))
|
||
|
||
if (res.data.summary) {
|
||
addLog(t('log.configSummaryAgents', { count: res.data.summary.total_agents }))
|
||
addLog(t('log.configSummaryHours', { hours: res.data.summary.simulation_hours }))
|
||
addLog(t('log.configSummaryPostsAlt', { count: res.data.summary.initial_posts_count }))
|
||
}
|
||
|
||
addLog(t('log.envSetupComplete'))
|
||
phase.value = 4
|
||
emit('update-status', 'completed')
|
||
} else {
|
||
// Config not generated yet — kick off polling.
|
||
addLog(t('log.configGenerating'))
|
||
startConfigPolling()
|
||
}
|
||
}
|
||
} catch (err) {
|
||
addLog(t('log.loadConfigFailed', { error: 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(t('log.step2Init'))
|
||
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;
|
||
}
|
||
|
||
/* Basic-info grid */
|
||
.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;
|
||
}
|
||
|
||
/* Module section */
|
||
.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;
|
||
}
|
||
|
||
/* Topic tags */
|
||
.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;
|
||
}
|
||
|
||
/* Detailed persona */
|
||
.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;
|
||
}
|
||
|
||
/* Round-count config styles */
|
||
.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 !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 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>
|