diff --git a/frontend/src/components/Step3Simulation.vue b/frontend/src/components/Step3Simulation.vue index 2afba1a..7ddbd29 100644 --- a/frontend/src/components/Step3Simulation.vue +++ b/frontend/src/components/Step3Simulation.vue @@ -52,18 +52,28 @@
+ +
+
+ 共 {{ allActions.length }} 条动作 + + + 📮 {{ redditActionsCount }} + +
+
+
@@ -71,7 +81,7 @@
@@ -88,25 +98,99 @@
- -
+ +
{{ action.action_args.content }}
- -
-
- Replying to @{{ action.action_args.original_author_name || 'User' }} -
-
+ + - -
- Liked Post: - "{{ truncateContent(action.action_args.post_content) }}" + + + + + + + + + + + + + + + + + + + + + + +
+ {{ action.action_args.content }}
@@ -118,9 +202,9 @@
-
+
- Waiting for agent actions... + 等待 Agent 行动中...
@@ -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;