Enhance Step3Simulation component with detailed action tracking and UI improvements

- Updated the action display logic to include various action types such as CREATE_POST, QUOTE_POST, REPOST, and LIKE_POST, providing clearer user feedback.
- Introduced a timeline header to summarize action counts and platform breakdowns, improving the overall user interface.
- Refactored the action handling to ensure unique identification and prevent duplicates, enhancing performance and reliability.
- Improved button styling and text for better visibility and user interaction during the simulation process.
This commit is contained in:
666ghj 2025-12-12 17:08:23 +08:00
parent d768fd1ea2
commit c91dad30db

View file

@ -52,18 +52,28 @@
<div class="action-controls">
<button
class="ctrl-btn next"
class="action-btn primary"
:disabled="phase !== 2"
@click="handleNextStep"
>
<span v-if="phase !== 2" class="spinner-sm running"></span>
{{ phase === 2 ? 'GENERATE REPORT ➝' : 'SIMULATING...' }}
开始生成结果报告
</button>
</div>
</div>
<!-- Main Content: Dual Timeline -->
<div class="main-content-area" ref="scrollContainer">
<!-- Timeline Header -->
<div class="timeline-header" v-if="allActions.length > 0">
<div class="timeline-stats">
<span class="total-count"> {{ allActions.length }} 条动作</span>
<span class="platform-breakdown">
<span class="twitter-count">𝕏 {{ twitterActionsCount }}</span>
<span class="reddit-count">📮 {{ redditActionsCount }}</span>
</span>
</div>
</div>
<!-- Timeline Feed -->
<div class="timeline-feed">
<div class="timeline-axis"></div>
@ -71,7 +81,7 @@
<TransitionGroup name="timeline-item">
<div
v-for="action in reversedActions"
:key="action.id || `${action.timestamp}-${action.agent_id}`"
:key="action._uniqueId || action.id || `${action.timestamp}-${action.agent_id}`"
class="timeline-item"
:class="action.platform"
>
@ -88,25 +98,99 @@
</div>
<div class="card-body">
<!-- Main Content -->
<div v-if="action.action_args?.content" class="content-text">
<!-- CREATE_POST: 发布帖子 -->
<div v-if="action.action_type === 'CREATE_POST' && action.action_args?.content" class="content-text">
{{ action.action_args.content }}
</div>
<!-- Quote / Repost Content -->
<div v-if="action.action_args?.quote_content" class="quoted-block">
<div class="quote-author">
Replying to @{{ action.action_args.original_author_name || 'User' }}
</div>
<div class="quote-text">
<!-- QUOTE_POST: 引用帖子 -->
<template v-if="action.action_type === 'QUOTE_POST'">
<div v-if="action.action_args?.quote_content" class="content-text">
{{ action.action_args.quote_content }}
</div>
<div v-if="action.action_args?.original_content" class="quoted-block">
<div class="quote-header">
<span class="quote-icon">🔗</span>
<span class="quote-label">引用 @{{ action.action_args.original_author_name || 'User' }}</span>
</div>
<div class="quote-text">
{{ truncateContent(action.action_args.original_content, 150) }}
</div>
</div>
</template>
<!-- Target Context (e.g. for Likes) -->
<div v-if="action.action_type?.includes('LIKE') && action.action_args?.post_content" class="target-context">
<span class="context-label">Liked Post:</span>
"{{ truncateContent(action.action_args.post_content) }}"
<!-- REPOST: 转发帖子 -->
<template v-if="action.action_type === 'REPOST'">
<div class="repost-info">
<span class="repost-icon">🔄</span>
<span class="repost-label">转发自 @{{ action.action_args?.original_author_name || 'User' }}</span>
</div>
<div v-if="action.action_args?.original_content" class="repost-content">
{{ truncateContent(action.action_args.original_content, 200) }}
</div>
</template>
<!-- LIKE_POST: 点赞帖子 -->
<template v-if="action.action_type === 'LIKE_POST'">
<div class="like-info">
<span class="like-icon"></span>
<span class="like-label">点赞了 @{{ action.action_args?.post_author_name || 'User' }} 的帖子</span>
</div>
<div v-if="action.action_args?.post_content" class="liked-content">
"{{ truncateContent(action.action_args.post_content, 120) }}"
</div>
</template>
<!-- CREATE_COMMENT: 发表评论 -->
<template v-if="action.action_type === 'CREATE_COMMENT'">
<div v-if="action.action_args?.content" class="content-text">
{{ action.action_args.content }}
</div>
<div v-if="action.action_args?.post_id" class="comment-context">
<span class="context-icon">💬</span>
<span>评论于帖子 #{{ action.action_args.post_id }}</span>
</div>
</template>
<!-- SEARCH_POSTS: 搜索帖子 -->
<template v-if="action.action_type === 'SEARCH_POSTS'">
<div class="search-info">
<span class="search-icon">🔍</span>
<span class="search-label">搜索关键词:</span>
<span class="search-query">"{{ action.action_args?.query || '' }}"</span>
</div>
</template>
<!-- FOLLOW: 关注用户 -->
<template v-if="action.action_type === 'FOLLOW'">
<div class="follow-info">
<span class="follow-icon">👤</span>
<span class="follow-label">关注了用户 @{{ action.action_args?.target_user || action.action_args?.user_id || 'User' }}</span>
</div>
</template>
<!-- UPVOTE / DOWNVOTE -->
<template v-if="action.action_type === 'UPVOTE_POST' || action.action_type === 'DOWNVOTE_POST'">
<div class="vote-info">
<span class="vote-icon">{{ action.action_type === 'UPVOTE_POST' ? '👍' : '👎' }}</span>
<span class="vote-label">{{ action.action_type === 'UPVOTE_POST' ? '赞同' : '反对' }}了帖子</span>
</div>
<div v-if="action.action_args?.post_content" class="voted-content">
"{{ truncateContent(action.action_args.post_content, 120) }}"
</div>
</template>
<!-- DO_NOTHING: 无操作静默 -->
<template v-if="action.action_type === 'DO_NOTHING'">
<div class="idle-info">
<span class="idle-icon">💤</span>
<span class="idle-label">此轮无活动</span>
</div>
</template>
<!-- 通用回退未知类型或有 content 但未被上述处理 -->
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
{{ action.action_args.content }}
</div>
</div>
@ -118,9 +202,9 @@
</div>
</TransitionGroup>
<div v-if="recentActions.length === 0" class="waiting-state">
<div v-if="allActions.length === 0" class="waiting-state">
<div class="pulse-ring"></div>
<span>Waiting for agent actions...</span>
<span>等待 Agent 行动中...</span>
</div>
</div>
</div>
@ -166,13 +250,23 @@ const isStarting = ref(false)
const isStopping = ref(false)
const startError = ref(null)
const runStatus = ref({})
const recentActions = ref([])
const allActions = ref([]) //
const actionIds = ref(new Set()) // ID
const scrollContainer = ref(null)
// Computed
// Reverse actions to show newest at top
//
const reversedActions = computed(() => {
return [...recentActions.value]
return [...allActions.value].reverse()
})
//
const twitterActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'twitter').length
})
const redditActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'reddit').length
})
// Methods
@ -184,7 +278,8 @@ const addLog = (msg) => {
const resetAllState = () => {
phase.value = 0
runStatus.value = {}
recentActions.value = []
allActions.value = []
actionIds.value = new Set()
prevTwitterRound.value = 0
prevRedditRound.value = 0
startError.value = null
@ -374,9 +469,32 @@ const fetchRunStatusDetail = async () => {
try {
const res = await getRunStatusDetail(props.simulationId)
if (res.success && res.data?.recent_actions) {
// Keep only last 50 actions for performance
recentActions.value = res.data.recent_actions.slice(0, 50)
if (res.success && res.data) {
// 使 all_actions
const serverActions = res.data.all_actions || []
//
let newActionsAdded = 0
serverActions.forEach(action => {
// ID
const actionId = action.id || `${action.timestamp}-${action.platform}-${action.agent_id}-${action.action_type}`
if (!actionIds.value.has(actionId)) {
actionIds.value.add(actionId)
allActions.value.push({
...action,
_uniqueId: actionId
})
newActionsAdded++
}
})
//
if (newActionsAdded > 0 && scrollContainer.value) {
nextTick(() => {
scrollContainer.value.scrollTop = 0
})
}
}
} catch (err) {
console.warn('获取详细状态失败:', err)
@ -386,17 +504,19 @@ const fetchRunStatusDetail = async () => {
// Helpers
const getActionTypeLabel = (type) => {
const labels = {
'CREATE_POST': 'POST',
'REPOST': 'REPOST',
'LIKE_POST': 'LIKE',
'CREATE_COMMENT': 'COMMENT',
'LIKE_COMMENT': 'LIKE',
'DO_NOTHING': 'IDLE',
'FOLLOW': 'FOLLOW',
'SEARCH_POSTS': 'SEARCH',
'QUOTE_POST': 'QUOTE'
'CREATE_POST': '发帖',
'REPOST': '转发',
'LIKE_POST': '点赞',
'CREATE_COMMENT': '评论',
'LIKE_COMMENT': '点赞',
'DO_NOTHING': '静默',
'FOLLOW': '关注',
'SEARCH_POSTS': '搜索',
'QUOTE_POST': '引用',
'UPVOTE_POST': '赞同',
'DOWNVOTE_POST': '反对'
}
return labels[type] || type || 'UNKNOWN'
return labels[type] || type || '未知'
}
const getActionTypeClass = (type) => {
@ -406,14 +526,19 @@ const getActionTypeClass = (type) => {
'LIKE_POST': 'badge-like',
'CREATE_COMMENT': 'badge-comment',
'LIKE_COMMENT': 'badge-like',
'QUOTE_POST': 'badge-quote'
'QUOTE_POST': 'badge-quote',
'FOLLOW': 'badge-follow',
'SEARCH_POSTS': 'badge-search',
'UPVOTE_POST': 'badge-upvote',
'DOWNVOTE_POST': 'badge-downvote',
'DO_NOTHING': 'badge-idle'
}
return classes[type] || 'badge-default'
}
const truncateContent = (content) => {
const truncateContent = (content, maxLength = 100) => {
if (!content) return ''
if (content.length > 100) return content.substring(0, 100) + '...'
if (content.length > maxLength) return content.substring(0, maxLength) + '...'
return content
}
@ -570,31 +695,31 @@ onUnmounted(() => {
margin-left: 4px;
}
.ctrl-btn {
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
font-size: 12px;
border: none;
cursor: pointer;
display: flex;
/* Action Button - Step2 Style */
.action-btn {
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.ctrl-btn.next {
background: #E8F5E9;
color: #2E7D32;
.action-btn.primary {
background: #000;
color: #FFF;
}
.ctrl-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.action-btn.primary:hover:not(:disabled) {
opacity: 0.8;
}
.ctrl-btn:disabled {
opacity: 0.6;
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@ -606,6 +731,46 @@ onUnmounted(() => {
background: #FAFAFA;
}
/* Timeline Header */
.timeline-header {
position: sticky;
top: 0;
background: rgba(250, 250, 250, 0.95);
backdrop-filter: blur(8px);
padding: 12px 24px;
border-bottom: 1px solid #E0E0E0;
z-index: 5;
}
.timeline-stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-count {
font-size: 13px;
font-weight: 600;
color: #333;
}
.platform-breakdown {
display: flex;
gap: 16px;
}
.twitter-count {
font-size: 12px;
color: #1DA1F2;
font-weight: 500;
}
.reddit-count {
font-size: 12px;
color: #FF5722;
font-weight: 500;
}
/* --- Timeline Feed --- */
.timeline-feed {
padding: 24px;
@ -728,6 +893,11 @@ onUnmounted(() => {
.badge-like { background: #FFEBEE; color: #C62828; }
.badge-repost { background: #E8F5E9; color: #2E7D32; }
.badge-comment { background: #FFF3E0; color: #E65100; }
.badge-follow { background: #FCE4EC; color: #AD1457; }
.badge-search { background: #E8EAF6; color: #3949AB; }
.badge-upvote { background: #E8F5E9; color: #388E3C; }
.badge-downvote { background: #FFEBEE; color: #D32F2F; }
.badge-idle { background: #ECEFF1; color: #607D8B; }
.badge-default { background: #F5F5F5; color: #757575; }
.content-text {
@ -737,26 +907,198 @@ onUnmounted(() => {
margin-bottom: 8px;
}
/* Quote Block */
.quoted-block {
background: #F9F9F9;
background: #F8F9FA;
border-left: 3px solid #DDD;
padding: 8px 12px;
border-radius: 0 4px 4px 0;
margin-top: 8px;
padding: 10px 12px;
border-radius: 0 6px 6px 0;
margin-top: 10px;
}
.quote-author {
.quote-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.quote-icon {
font-size: 12px;
}
.quote-label {
font-size: 11px;
color: #666;
margin-bottom: 4px;
font-weight: 500;
}
.quote-text {
font-size: 12px;
color: #555;
line-height: 1.5;
}
/* Repost Styles */
.repost-info {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: #2E7D32;
}
.repost-icon {
font-size: 14px;
}
.repost-label {
font-size: 12px;
font-weight: 500;
}
.repost-content {
background: #F1F8E9;
padding: 10px 12px;
border-radius: 6px;
font-size: 12px;
color: #555;
line-height: 1.5;
}
/* Like Styles */
.like-info {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: #C62828;
}
.like-icon {
font-size: 14px;
}
.like-label {
font-size: 12px;
font-weight: 500;
}
.liked-content {
background: #FFEBEE;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
color: #666;
font-style: italic;
}
/* Comment Styles */
.comment-context {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 11px;
color: #666;
}
.context-icon {
font-size: 12px;
}
/* Search Styles */
.search-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #E3F2FD;
border-radius: 6px;
}
.search-icon {
font-size: 16px;
}
.search-label {
font-size: 12px;
color: #666;
}
.search-query {
font-size: 13px;
font-weight: 600;
color: #1565C0;
}
/* Follow Styles */
.follow-info {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #F3E5F5;
border-radius: 6px;
color: #7B1FA2;
}
.follow-icon {
font-size: 14px;
}
.follow-label {
font-size: 12px;
font-weight: 500;
}
/* Vote Styles */
.vote-info {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.vote-icon {
font-size: 14px;
}
.vote-label {
font-size: 12px;
font-weight: 500;
color: #666;
}
.voted-content {
background: #F5F5F5;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
color: #666;
font-style: italic;
}
/* Idle Styles */
.idle-info {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #F5F5F5;
border-radius: 6px;
color: #999;
}
.idle-icon {
font-size: 14px;
}
.idle-label {
font-size: 12px;
}
/* Target Context (Legacy) */
.target-context {
font-size: 11px;
color: #666;