feat(i18n): replace hardcoded Chinese in frontend components with i18n calls

Replace all user-visible hardcoded Chinese strings in 7 component files
with $t() / t() calls using vue-i18n:

- Step1GraphBuild: ontology generation, graph build, status badges
- Step2EnvSetup: simulation setup, agent personas, platform config,
  time config, initial activation, modal profile details
- Step3Simulation: report generation button
- Step4Report: section loading text, interaction button
- Step5Interaction: chat interface, survey UI, tool descriptions,
  error messages, agent selection
- GraphPanel: graph status hints, loading states, tooltips
- HistoryDatabase: history cards, modal, replay buttons

Added missing translation keys to both zh.json and en.json locale files.
Added useI18n imports to components that need script-level translations.
This commit is contained in:
ghostubborn
2026-04-01 15:43:11 +08:00
parent 70833821a2
commit fc47ae81b5
9 changed files with 253 additions and 208 deletions

View File

@@ -58,7 +58,7 @@
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="4" stroke="#4B5563" stroke-linecap="round"></path>
</svg>
</div>
<span class="loading-text">正在生成{{ section.title }}...</span>
<span class="loading-text">{{ $t('step4.generatingSection', { title: section.title }) }}</span>
</div>
</div>
</div>
@@ -85,8 +85,8 @@
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<div class="action-bar-text">
<span class="action-bar-title">Interactive Tools</span>
<span class="action-bar-subtitle mono">{{ profiles.length }} agents available</span>
<span class="action-bar-title">{{ $t('step5.interactiveTools') }}</span>
<span class="action-bar-subtitle mono">{{ $t('step5.agentsAvailable', { count: profiles.length }) }}</span>
</div>
</div>
<div class="action-bar-tabs">
@@ -98,7 +98,7 @@
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
</svg>
<span>Report Agent对话</span>
<span>{{ $t('step5.chatWithReportAgent') }}</span>
</button>
<div class="agent-dropdown" v-if="profiles.length > 0">
<button
@@ -110,13 +110,13 @@
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span>{{ selectedAgent ? selectedAgent.username : '与世界中任意个体对话' }}</span>
<span>{{ selectedAgent ? selectedAgent.username : $t('step5.chatWithAgent') }}</span>
<svg class="dropdown-arrow" :class="{ open: showAgentDropdown }" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div v-if="showAgentDropdown" class="dropdown-menu">
<div class="dropdown-header">选择对话对象</div>
<div class="dropdown-header">{{ $t('step5.selectChatTarget') }}</div>
<div
v-for="(agent, idx) in profiles"
:key="idx"
@@ -126,13 +126,13 @@
<div class="agent-avatar">{{ (agent.username || 'A')[0] }}</div>
<div class="agent-info">
<span class="agent-name">{{ agent.username }}</span>
<span class="agent-role">{{ agent.profession || '未知职业' }}</span>
<span class="agent-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
</div>
</div>
</div>
</div>
<div class="tab-divider"></div>
<button
<button
class="tab-pill survey-pill"
:class="{ active: activeTab === 'survey' }"
@click="selectSurveyTab"
@@ -141,7 +141,7 @@
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
<span>发送问卷调查到世界中</span>
<span>{{ $t('step5.sendSurvey') }}</span>
</button>
</div>
</div>
@@ -154,8 +154,8 @@
<div class="tools-card-header">
<div class="tools-card-avatar">R</div>
<div class="tools-card-info">
<div class="tools-card-name">Report Agent - Chat</div>
<div class="tools-card-subtitle">报告生成智能体的快速对话版本可调用 4 种专业工具拥有MiroFish的完整记忆</div>
<div class="tools-card-name">{{ $t('step5.reportAgentChat') }}</div>
<div class="tools-card-subtitle">{{ $t('step5.reportAgentDesc') }}</div>
</div>
<button class="tools-card-toggle" @click="showToolsDetail = !showToolsDetail">
<svg :class="{ 'is-expanded': showToolsDetail }" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
@@ -172,8 +172,8 @@
</svg>
</div>
<div class="tool-content">
<div class="tool-name">InsightForge 深度归因</div>
<div class="tool-desc">对齐现实世界种子数据与模拟环境状态结合Global/Local Memory机制提供跨时空的深度归因分析</div>
<div class="tool-name">{{ $t('step5.toolInsightForge') }}</div>
<div class="tool-desc">{{ $t('step5.toolInsightForgeDesc') }}</div>
</div>
</div>
<div class="tool-item tool-blue">
@@ -184,8 +184,8 @@
</svg>
</div>
<div class="tool-content">
<div class="tool-name">PanoramaSearch 全景追踪</div>
<div class="tool-desc">基于图结构的广度遍历算法重构事件传播路径捕获全量信息流动的拓扑结构</div>
<div class="tool-name">{{ $t('step5.toolPanoramaSearch') }}</div>
<div class="tool-desc">{{ $t('step5.toolPanoramaSearchDesc') }}</div>
</div>
</div>
<div class="tool-item tool-orange">
@@ -195,8 +195,8 @@
</svg>
</div>
<div class="tool-content">
<div class="tool-name">QuickSearch 快速检索</div>
<div class="tool-desc">基于 GraphRAG 的即时查询接口优化索引效率用于快速提取具体的节点属性与离散事实</div>
<div class="tool-name">{{ $t('step5.toolQuickSearch') }}</div>
<div class="tool-desc">{{ $t('step5.toolQuickSearchDesc') }}</div>
</div>
</div>
<div class="tool-item tool-green">
@@ -208,8 +208,8 @@
</svg>
</div>
<div class="tool-content">
<div class="tool-name">InterviewSubAgent 虚拟访谈</div>
<div class="tool-desc">自主式访谈能够并行与模拟世界中个体进行多轮对话采集非结构化的观点数据与心理状态</div>
<div class="tool-name">{{ $t('step5.toolInterviewSubAgent') }}</div>
<div class="tool-desc">{{ $t('step5.toolInterviewSubAgentDesc') }}</div>
</div>
</div>
</div>
@@ -224,7 +224,7 @@
<div class="profile-card-name">{{ selectedAgent.username }}</div>
<div class="profile-card-meta">
<span v-if="selectedAgent.name" class="profile-card-handle">@{{ selectedAgent.name }}</span>
<span class="profile-card-profession">{{ selectedAgent.profession || '未知职业' }}</span>
<span class="profile-card-profession">{{ selectedAgent.profession || $t('step2.unknownProfession') }}</span>
</div>
</div>
<button class="profile-card-toggle" @click="showFullProfile = !showFullProfile">
@@ -235,7 +235,7 @@
</div>
<div v-if="showFullProfile && selectedAgent.bio" class="profile-card-body">
<div class="profile-card-bio">
<div class="profile-card-label">简介</div>
<div class="profile-card-label">{{ $t('step5.profileBio') }}</div>
<p>{{ selectedAgent.bio }}</p>
</div>
</div>
@@ -250,7 +250,7 @@
</svg>
</div>
<p class="empty-text">
{{ chatTarget === 'report_agent' ? '与 Report Agent 对话,深入了解报告内容' : '与模拟个体对话,了解他们的观点' }}
{{ chatTarget === 'report_agent' ? $t('step5.chatEmptyReportAgent') : $t('step5.chatEmptyAgent') }}
</p>
</div>
<div
@@ -292,7 +292,7 @@
<textarea
v-model="chatInput"
class="chat-input"
placeholder="输入您的问题..."
:placeholder="$t('step5.chatInputPlaceholder')"
@keydown.enter.exact.prevent="sendMessage"
:disabled="isSending || (!selectedAgent && chatTarget === 'agent')"
rows="1"
@@ -317,8 +317,8 @@
<div class="survey-setup">
<div class="setup-section">
<div class="section-header">
<span class="section-title">选择调查对象</span>
<span class="selection-count">已选 {{ selectedAgents.size }} / {{ profiles.length }}</span>
<span class="section-title">{{ $t('step5.selectSurveyTarget') }}</span>
<span class="selection-count">{{ $t('step5.selectedCount', { selected: selectedAgents.size, total: profiles.length }) }}</span>
</div>
<div class="agents-grid">
<label
@@ -335,7 +335,7 @@
<div class="checkbox-avatar">{{ (agent.username || 'A')[0] }}</div>
<div class="checkbox-info">
<span class="checkbox-name">{{ agent.username }}</span>
<span class="checkbox-role">{{ agent.profession || '未知职业' }}</span>
<span class="checkbox-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
</div>
<div class="checkbox-indicator">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
@@ -345,20 +345,20 @@
</label>
</div>
<div class="selection-actions">
<button class="action-link" @click="selectAllAgents">全选</button>
<button class="action-link" @click="selectAllAgents">{{ $t('step5.selectAll') }}</button>
<span class="action-divider">|</span>
<button class="action-link" @click="clearAgentSelection">清空</button>
<button class="action-link" @click="clearAgentSelection">{{ $t('step5.clearSelection') }}</button>
</div>
</div>
<div class="setup-section">
<div class="section-header">
<span class="section-title">问卷问题</span>
<span class="section-title">{{ $t('step5.surveyQuestions') }}</span>
</div>
<textarea
v-model="surveyQuestion"
class="survey-input"
placeholder="输入您想问所有被选中对象的问题..."
:placeholder="$t('step5.surveyInputPlaceholder')"
rows="3"
></textarea>
</div>
@@ -369,15 +369,15 @@
@click="submitSurvey"
>
<span v-if="isSurveying" class="loading-spinner"></span>
<span v-else>发送问卷</span>
<span v-else>{{ $t('step5.submitSurvey') }}</span>
</button>
</div>
<!-- Survey Results -->
<div v-if="surveyResults.length > 0" class="survey-results">
<div class="results-header">
<span class="results-title">调查结果</span>
<span class="results-count">{{ surveyResults.length }} 条回复</span>
<span class="results-title">{{ $t('step5.surveyResults') }}</span>
<span class="results-count">{{ $t('step5.surveyResultsCount', { count: surveyResults.length }) }}</span>
</div>
<div class="results-list">
<div
@@ -389,7 +389,7 @@
<div class="result-avatar">{{ (result.agent_name || 'A')[0] }}</div>
<div class="result-info">
<span class="result-name">{{ result.agent_name }}</span>
<span class="result-role">{{ result.profession || '未知职业' }}</span>
<span class="result-role">{{ result.profession || $t('step2.unknownProfession') }}</span>
</div>
</div>
<div class="result-question">
@@ -412,9 +412,12 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { chatWithReport, getReport, getAgentLog } from '../api/report'
import { interviewAgents, getSimulationProfilesRealtime } from '../api/simulation'
const { t } = useI18n()
const props = defineProps({
reportId: String,
simulationId: String
@@ -665,7 +668,7 @@ const sendMessage = async () => {
addLog(`发送失败: ${err.message}`)
chatHistory.value.push({
role: 'assistant',
content: `抱歉,发生了错误: ${err.message}`,
content: t('step5.errorOccurred', { error: err.message }),
timestamp: new Date().toISOString()
})
} finally {
@@ -697,18 +700,18 @@ const sendToReportAgent = async (message) => {
if (res.success && res.data) {
chatHistory.value.push({
role: 'assistant',
content: res.data.response || res.data.answer || '无响应',
content: res.data.response || res.data.answer || t('step5.noResponse'),
timestamp: new Date().toISOString()
})
addLog('Report Agent 已回复')
} else {
throw new Error(res.error || '请求失败')
throw new Error(res.error || t('step5.requestFailed'))
}
}
const sendToAgent = async (message) => {
if (!selectedAgent.value || selectedAgentIndex.value === null) {
throw new Error('请先选择一个模拟个体')
throw new Error(t('step5.selectAgentFirst'))
}
addLog(`${selectedAgent.value.username} 发送: ${message.substring(0, 50)}...`)
@@ -763,10 +766,10 @@ const sendToAgent = async (message) => {
})
addLog(`${selectedAgent.value.username} 已回复`)
} else {
throw new Error('无响应数据')
throw new Error(t('step5.noResponse'))
}
} else {
throw new Error(res.error || '请求失败')
throw new Error(res.error || t('step5.requestFailed'))
}
}
@@ -830,20 +833,20 @@ const submitSurvey = async () => {
const agent = profiles.value[agentIdx]
// 优先使用 reddit 平台回复,其次 twitter
let responseContent = '无响应'
let responseContent = t('step5.noResponse')
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
const redditKey = `reddit_${agentIdx}`
const twitterKey = `twitter_${agentIdx}`
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey]
if (agentResult) {
responseContent = agentResult.response || agentResult.answer || '无响应'
responseContent = agentResult.response || agentResult.answer || t('step5.noResponse')
}
} else if (Array.isArray(resultsDict)) {
// 兼容数组格式
const matchedResult = resultsDict.find(r => r.agent_id === agentIdx)
if (matchedResult) {
responseContent = matchedResult.response || matchedResult.answer || '无响应'
responseContent = matchedResult.response || matchedResult.answer || t('step5.noResponse')
}
}
@@ -859,7 +862,7 @@ const submitSurvey = async () => {
surveyResults.value = surveyResultsList
addLog(`收到 ${surveyResults.value.length} 条回复`)
} else {
throw new Error(res.error || '请求失败')
throw new Error(res.error || t('step5.requestFailed'))
}
} catch (err) {
addLog(`问卷发送失败: ${err.message}`)