Merge pull request #30 from salestech-group/fix/23-externalize-chinese-frontend-strings
fix(i18n): externalize chinese ui strings in process and step views
This commit is contained in:
commit
af9381b359
|
|
@ -0,0 +1,544 @@
|
||||||
|
# Technical Design — `i18n-frontend-ui-strings`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Purpose**: Externalize the remaining hard-coded Chinese UI strings in five frontend Vue files (`Process.vue`, `Step2EnvSetup.vue`, `Step3Simulation.vue`, `Step4Report.vue`, `Step5Interaction.vue`) to `vue-i18n` keys, restructure backend-coupled regex parsers in `Step4Report.vue` so they survive the upcoming backend prompt translation, and add a small audit script to verify acceptance.
|
||||||
|
|
||||||
|
**Users**: English-locale users of the MiroFish UI (the production tablet/desktop dashboard). No backend or API consumer is affected.
|
||||||
|
|
||||||
|
**Impact**: Translates ~50 user-visible strings, refactors three string-equality stage checks into a lookup, and centralizes 29 backend-coupled regexes into a top-of-file constants block. No behaviour change for Chinese-locale users.
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Every flagged `file:line` from issue #23 either substituted with a `t()` call backed by entries in both `locales/en.json` and `locales/zh.json`, **or** explicitly classified as a deliberate Chinese token (parser marker for backend compatibility) and added to the audit allowlist.
|
||||||
|
- `Step4Report.vue` parsers continue to function while the backend remains 100% Chinese, **and** are positioned for a single-file update when the backend prompt translation lands.
|
||||||
|
- `Step2EnvSetup.vue` stage-watcher tolerates legacy Chinese display strings, current snake_case identifiers, and any future English display strings without further frontend edits.
|
||||||
|
- A `frontend/scripts/audit-i18n-strings.sh` (or `.js`) check runs in under a minute, requires no backend, and reports zero unallowlisted CJK literals across the five files.
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- Translating backend log messages, ontology/report agent prompts, or other backend code (covered by issues #24, #25 and the open `i18n-*-prompts` specs).
|
||||||
|
- Translating Chinese comments in source files (covered by issues #7 and #9).
|
||||||
|
- Frontend changes outside the five named files.
|
||||||
|
- Adding a CI gate for this audit (tracked under issue #26).
|
||||||
|
- Restoring the missing `.kiro/specs/i18n-e2e-english-verification/audit/scripts/run_audit.sh`.
|
||||||
|
|
||||||
|
## Boundary Commitments
|
||||||
|
|
||||||
|
### This Spec Owns
|
||||||
|
|
||||||
|
- `frontend/src/views/Process.vue`: every user-visible Chinese literal flagged in the ticket and any sibling Chinese literal discovered while editing the same block.
|
||||||
|
- `frontend/src/components/Step2EnvSetup.vue`: stage-watcher logic at lines 678–692 (`STAGE_PHASE_MAP`).
|
||||||
|
- `frontend/src/components/Step3Simulation.vue`: the `'启动失败'` fallback at line 423/427.
|
||||||
|
- `frontend/src/components/Step4Report.vue`: all 29 regex parser markers (lines 555–943), the no-reply marker checks (lines 850/854/1325), the `'选择理由'` literal (line 1464), the `'等待开始'` literal (line 1774), and the log-classification literals (lines 2005–2006).
|
||||||
|
- `frontend/src/components/Step5Interaction.vue`: the chat-history templating (lines 721, 723).
|
||||||
|
- `locales/en.json` and `locales/zh.json`: new keys added by this spec, mirrored across both files with structurally aligned shape.
|
||||||
|
- `frontend/scripts/audit-i18n-strings.sh` (new): the small grep-based verifier for Requirement 6.
|
||||||
|
|
||||||
|
### Out of Boundary
|
||||||
|
|
||||||
|
- Backend prompt strings in `backend/app/services/zep_tools.py`, `report_agent.py`, etc. — the responsibility of `i18n-report-agent-prompts` and issue #25.
|
||||||
|
- Other Vue files. Even if their templates also contain Chinese literals, they are out of scope for this spec.
|
||||||
|
- Vue Router, auth, telemetry, accessibility — not affected.
|
||||||
|
- A more elaborate keys-parity tool — explicit non-goal; the existing `wc -l` agreement and a one-liner `jq` diff suffice.
|
||||||
|
|
||||||
|
### Allowed Dependencies
|
||||||
|
|
||||||
|
- `vue-i18n` 11 (already adopted; default and fallback locale `'zh'`; messages loaded from `/locales/*.json`).
|
||||||
|
- `frontend/src/i18n/index.js` (no changes).
|
||||||
|
- `locales/{en,zh,languages}.json` (new keys only).
|
||||||
|
- The structure of backend-emitted markers in `zep_tools.py` (read-only reference; not modified here).
|
||||||
|
|
||||||
|
### Revalidation Triggers
|
||||||
|
|
||||||
|
- The downstream backend prompt translation (issue #25 / `i18n-report-agent-prompts`) lands. → Update `REPORT_MARKERS` in `Step4Report.vue` to alternate Chinese/English wording (single-file edit).
|
||||||
|
- A new pipeline stage is added in `Step2EnvSetup.vue`. → Add a row to `STAGE_PHASE_MAP`.
|
||||||
|
- `locales/en.json` or `locales/zh.json` shape changes (new namespace, key removal). → Re-run `audit-i18n-strings.sh` and the parity diff.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Existing Architecture Analysis
|
||||||
|
|
||||||
|
The frontend already uses `vue-i18n` 11 throughout four of the five files. The pattern is uniform: `<template>` uses `$t()`, `<script setup>` uses `const { t } = useI18n()` then `t()`. The file `frontend/src/i18n/index.js` constructs the `createI18n` instance from `import.meta.glob('/locales/*.json')`. There is **no SSR**, no async-loaded translation chunks, and no per-route message-tree splitting — all keys are eagerly loaded.
|
||||||
|
|
||||||
|
`Process.vue` is the sole outlier: it has zero i18n adoption today. It will receive the entire pattern (a `useI18n()` import, then template + script substitutions). No structural change to the file beyond the substitutions and the new import.
|
||||||
|
|
||||||
|
### Architecture Pattern & Boundary Map
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph FE[Frontend (Vue 3 + vue-i18n 11)]
|
||||||
|
direction TB
|
||||||
|
Process[views/Process.vue]
|
||||||
|
Step2[components/Step2EnvSetup.vue]
|
||||||
|
Step3[components/Step3Simulation.vue]
|
||||||
|
Step4[components/Step4Report.vue]
|
||||||
|
Step5[components/Step5Interaction.vue]
|
||||||
|
i18nIdx[i18n/index.js]
|
||||||
|
Audit[scripts/audit-i18n-strings.sh]
|
||||||
|
end
|
||||||
|
subgraph Locales[/locales/]
|
||||||
|
En[en.json]
|
||||||
|
Zh[zh.json]
|
||||||
|
end
|
||||||
|
subgraph BE[Backend (Python)]
|
||||||
|
ZepTools[services/zep_tools.py]
|
||||||
|
end
|
||||||
|
|
||||||
|
Process -->|t-key lookup| i18nIdx
|
||||||
|
Step2 -->|t-key lookup| i18nIdx
|
||||||
|
Step3 -->|t-key lookup| i18nIdx
|
||||||
|
Step4 -->|t-key lookup| i18nIdx
|
||||||
|
Step5 -->|t-key lookup| i18nIdx
|
||||||
|
i18nIdx --> En
|
||||||
|
i18nIdx --> Zh
|
||||||
|
|
||||||
|
ZepTools -. emits Chinese markers .-> Step4
|
||||||
|
Step4 -- REPORT_MARKERS regex set --> Step4
|
||||||
|
|
||||||
|
Audit -. greps CJK literals minus allowlist .-> Process
|
||||||
|
Audit -. greps CJK literals minus allowlist .-> Step2
|
||||||
|
Audit -. greps CJK literals minus allowlist .-> Step3
|
||||||
|
Audit -. greps CJK literals minus allowlist .-> Step4
|
||||||
|
Audit -. greps CJK literals minus allowlist .-> Step5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture Integration**:
|
||||||
|
|
||||||
|
- **Selected pattern**: pure extension of the existing `vue-i18n` pattern, with two micro-refactors: an in-file `STAGE_PHASE_MAP` constant (Step2) and an in-file `REPORT_MARKERS` constants block (Step4). Both keep responsibility inside the matching Step component, in line with the steering principle "pipeline-stage logic lives in the matching Step component."
|
||||||
|
- **Domain/feature boundaries**: each file owns its own substitutions; `locales/{en,zh}.json` are the shared translation surface; `audit-i18n-strings.sh` is the verification surface.
|
||||||
|
- **Existing patterns preserved**: `const { t } = useI18n()` in `<script setup>`; `$t()` in `<template>`; namespace structure (`step1.*`, `process.*` new, `graph.*` reused).
|
||||||
|
- **New components rationale**: `audit-i18n-strings.sh` is the only new file. It exists because the original audit script referenced in the ticket has been deleted; without a verifier, Requirement 6 cannot be discharged.
|
||||||
|
- **Steering compliance**: matches `tech.md` ("User-visible strings live in repo-root `/locales/*.json`") and `structure.md` ("Pipeline-aligned modules"). No new dependencies. No new abstractions across the layer boundary.
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
|
||||||
|
| Layer | Choice / Version | Role in Feature | Notes |
|
||||||
|
|-------|------------------|-----------------|-------|
|
||||||
|
| Frontend / CLI | `vue-i18n` 11 (already adopted) | Translates user-visible strings via the `t()` API | Global instance constructed in `frontend/src/i18n/index.js`; default + fallback locale `'zh'` |
|
||||||
|
| Backend / Services | (read-only reference) Python 3.11 / Flask 3 | Source of Chinese marker strings parsed by `Step4Report.vue` | `backend/app/services/zep_tools.py` is the canonical emitter; not modified by this spec |
|
||||||
|
| Data / Storage | `locales/en.json`, `locales/zh.json` | Translation source | 1031 lines each (aligned per #20); new keys added under existing namespaces |
|
||||||
|
| Messaging / Events | n/a | — | — |
|
||||||
|
| Infrastructure / Runtime | `bash` 5+ (or Node 18+) for `audit-i18n-strings.sh` | Verifies Requirement 6 | Pure ripgrep + jq one-liner |
|
||||||
|
|
||||||
|
## File Structure Plan
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── Step2EnvSetup.vue # MODIFIED — add STAGE_PHASE_MAP, drop Chinese stage equality checks
|
||||||
|
│ │ ├── Step3Simulation.vue # MODIFIED — replace '启动失败' fallback with t('step3.startFailed')
|
||||||
|
│ │ ├── Step4Report.vue # MODIFIED — add REPORT_MARKERS block, route literals through t()
|
||||||
|
│ │ └── Step5Interaction.vue # MODIFIED — route chat-history templating through t()
|
||||||
|
│ ├── views/
|
||||||
|
│ │ └── Process.vue # MODIFIED — add useI18n() import, route every flagged literal through t()
|
||||||
|
│ └── i18n/
|
||||||
|
│ └── index.js # UNCHANGED
|
||||||
|
└── scripts/
|
||||||
|
└── audit-i18n-strings.sh # NEW — Requirement 6 verifier
|
||||||
|
|
||||||
|
locales/
|
||||||
|
├── en.json # MODIFIED — add new keys for process.*, step3.startFailed (if missing), step4.*, step5.*
|
||||||
|
└── zh.json # MODIFIED — mirror new keys with the original Chinese wording
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
|
||||||
|
- `frontend/src/views/Process.vue` — add `import { useI18n } from 'vue-i18n'` and `const { t } = useI18n()` in `<script setup>`. Replace every flagged Chinese literal in template + script with `t('process.<key>')` or `t('graph.<key>')` (where `graph.*` already covers the surface). Substitute fallback literals (`'未命名'` → `t('process.fallbackNodeName')`, `'未知'` → `t('common.unknown')` — already exists). Replace the `alert('环境搭建功能开发中...')` with `alert(t('process.envSetupComingSoon'))`.
|
||||||
|
- `frontend/src/components/Step2EnvSetup.vue` — add `const STAGE_PHASE_MAP = { 'generating_profiles': 1, '生成Agent人设': 1, 'generating_config': 2, '生成模拟配置': 2, 'copying_scripts': 2, '准备模拟脚本': 2 }` near other module constants. Rewrite the `watch(currentStage, …)` body to `phase.value = STAGE_PHASE_MAP[newStage] ?? phase.value`, with the `t('log.startGeneratingConfig')` log emission gated on the transition into phase 2 (matching today's behaviour). The Chinese console-warning strings are non-user-visible — leave alone (out of scope per ticket boundary).
|
||||||
|
- `frontend/src/components/Step3Simulation.vue` — replace `'启动失败'` literal at line 423/427 with `t('step3.startFailed')` (key already exists in `en.json` per the locale audit; confirm during implementation).
|
||||||
|
- `frontend/src/components/Step4Report.vue` — add a `REPORT_MARKERS` constants block at the top of `<script setup>`. Replace each inline Chinese regex with a reference into `REPORT_MARKERS` (e.g., `text.match(REPORT_MARKERS.analysisQuery.regex)`). Route the user-visible Chinese literals (`'选择理由'`, `'等待开始'`, `'--'` no-data placeholder if it surfaces) through `t('step4.<key>')`. The `interview.redditAnswer !== '(该平台未获得回复)'` checks become `!REPORT_MARKERS.noReply.is(interview.redditAnswer)`. Log-classifier literals stay (deliberate, allowlisted).
|
||||||
|
- `frontend/src/components/Step5Interaction.vue` — replace lines 721/723 with `t('step5.chatRolePrompter')` / `t('step5.chatRoleYou')` / `t('step5.chatHistoryPrefix', { history: historyContext })` / `t('step5.chatNewQuestionPrefix', { message })`. The Chinese phrasing is preserved exactly in `zh.json` so the production Chinese path is byte-identical.
|
||||||
|
- `locales/en.json` and `locales/zh.json` — add the new keys grouped under existing namespaces. Both files updated in lockstep; `zh.json` carries the exact Chinese wording removed from the source files.
|
||||||
|
- `frontend/scripts/audit-i18n-strings.sh` — new shell script (≤30 lines). Greps the five files for any non-allowlisted CJK code points and exits non-zero on hits.
|
||||||
|
|
||||||
|
## System Flows
|
||||||
|
|
||||||
|
### Flow 1: Audit verifier (Requirement 6)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start[Run audit-i18n-strings.sh] --> Grep[ripgrep CJK regex over the 5 files]
|
||||||
|
Grep --> Filter[Filter out allowlist tokens<br/>e.g. REPORT_MARKERS literals,<br/>log severity literals]
|
||||||
|
Filter --> HasHits{Any remaining hits?}
|
||||||
|
HasHits -- yes --> Report[Print file:line:snippet for each hit]
|
||||||
|
Report --> Exit1[exit 1]
|
||||||
|
HasHits -- no --> Parity[Parity check: en.json vs zh.json keys]
|
||||||
|
Parity --> ParityOk{Sets equal?}
|
||||||
|
ParityOk -- yes --> Exit0[exit 0]
|
||||||
|
ParityOk -- no --> ReportParity[Print missing keys per file]
|
||||||
|
ReportParity --> Exit1
|
||||||
|
```
|
||||||
|
|
||||||
|
The allowlist is encoded in the script itself (a small array of literal strings the verifier accepts). `REPORT_MARKERS` constants and the log-severity literals (`'错误'`, `'警告'`) are the only entries. Any future addition requires editing the allowlist explicitly — no implicit acceptance.
|
||||||
|
|
||||||
|
### Flow 2: Stage-watcher (Requirement 4)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Backend[Backend emits stage string<br/>e.g. '生成Agent人设' or 'generating_profiles'] --> Watch[Step2EnvSetup.vue watcher]
|
||||||
|
Watch --> Map[STAGE_PHASE_MAP[newStage]]
|
||||||
|
Map --> Found{Match?}
|
||||||
|
Found -- yes --> Update[phase.value = mapped phase]
|
||||||
|
Found -- no --> Noop[phase.value unchanged]
|
||||||
|
Update --> SideEffect[Trigger startConfigPolling / addLog as today]
|
||||||
|
```
|
||||||
|
|
||||||
|
The map covers all three stage names in both their Chinese display form and snake_case identifier form (six entries total). When the backend translation lands an English form (e.g. `'generating profiles'` or any other wording), one row is added to the map.
|
||||||
|
|
||||||
|
## Requirements Traceability
|
||||||
|
|
||||||
|
| Requirement | Summary | Components | Interfaces | Flows |
|
||||||
|
|-------------|---------|------------|------------|-------|
|
||||||
|
| 1.1 | `Process.vue` template/script literals routed through `t()` | `Process.vue` | `useI18n().t` | — |
|
||||||
|
| 1.2 | `Process.vue` renders no Chinese under `en` locale | `Process.vue`, `audit-i18n-strings.sh` | `t()` lookup | Flow 1 |
|
||||||
|
| 1.3 | `Process.vue` Chinese unchanged under `zh` locale | `Process.vue`, `locales/zh.json` | `t()` fallback | — |
|
||||||
|
| 1.4 | Fallback names use translated keys | `Process.vue` | `t('process.fallbackNodeName')` | — |
|
||||||
|
| 1.5 | New literals added to both locales | `locales/{en,zh}.json` | — | Flow 1 (parity check) |
|
||||||
|
| 2.1 | Step3 `'启动失败'` routed through `t()` | `Step3Simulation.vue` | `t('step3.startFailed')` | — |
|
||||||
|
| 2.2 | Step4 user-visible literals routed through `t()` | `Step4Report.vue` | `t('step4.*')` | — |
|
||||||
|
| 2.3 | Step5 chat-history templating routed through `t()` | `Step5Interaction.vue` | `t('step5.chat*')` | — |
|
||||||
|
| 2.4 | Step components render no Chinese under `en` | All four | `t()` | Flow 1 |
|
||||||
|
| 2.5 | Step2 stage transitions preserved | `Step2EnvSetup.vue` | `STAGE_PHASE_MAP` | Flow 2 |
|
||||||
|
| 2.6 | Existing `useI18n()` is the translation utility | All four | — | — |
|
||||||
|
| 3.1 | New keys exist in both locale files | `locales/{en,zh}.json` | — | Flow 1 |
|
||||||
|
| 3.2 | `zh.json` preserves exact Chinese wording | `locales/zh.json` | — | — |
|
||||||
|
| 3.3 | `en.json` carries idiomatic English | `locales/en.json` | — | — |
|
||||||
|
| 3.4 | Keys grouped under existing namespaces | `locales/{en,zh}.json` | — | — |
|
||||||
|
| 3.5 | Locale files structurally aligned | `locales/{en,zh}.json` | — | Flow 1 |
|
||||||
|
| 4.1 | Stage matcher accepts Chinese, snake_case, future English | `Step2EnvSetup.vue` | `STAGE_PHASE_MAP` | Flow 2 |
|
||||||
|
| 4.2 | Chinese builds keep current behaviour | `Step2EnvSetup.vue` | `STAGE_PHASE_MAP` | Flow 2 |
|
||||||
|
| 4.3 | Removing Chinese stage names later doesn't break | `Step2EnvSetup.vue` | `STAGE_PHASE_MAP` | Flow 2 |
|
||||||
|
| 4.4 | Single source of truth for stage matching | `Step2EnvSetup.vue` | `STAGE_PHASE_MAP` | Flow 2 |
|
||||||
|
| 5.1–5.11 | Bilingual / future-bilingual parser tolerance | `Step4Report.vue` | `REPORT_MARKERS` | — |
|
||||||
|
| 6.1 | Verifier reports zero hard-coded Chinese | `audit-i18n-strings.sh` | shell exit code | Flow 1 |
|
||||||
|
| 6.2 | Verifier check documented | `design.md` (this section), `audit-i18n-strings.sh` header | — | Flow 1 |
|
||||||
|
| 6.3 | Deliberate Chinese tokens allowlisted | `audit-i18n-strings.sh` | inline allowlist | Flow 1 |
|
||||||
|
| 6.4 | Verifier runs locally in <1 minute, no backend | `audit-i18n-strings.sh` | shell | Flow 1 |
|
||||||
|
|
||||||
|
## Components and Interfaces
|
||||||
|
|
||||||
|
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|
||||||
|
|-----------|--------------|--------|--------------|--------------------------|-----------|
|
||||||
|
| `Process.vue` (modified) | Frontend / view | Workflow orchestrator; rendering pipeline-stage UI | 1.1–1.5, 3.1–3.5 | `vue-i18n` (P0); `locales/*.json` (P0) | State |
|
||||||
|
| `Step2EnvSetup.vue` (modified) | Frontend / step component | Setup step; tracks backend stage transitions | 2.5, 2.6, 4.1–4.4 | `vue-i18n` (P0) | State |
|
||||||
|
| `Step3Simulation.vue` (modified) | Frontend / step component | Simulation runner UI | 2.1, 2.4, 2.6 | `vue-i18n` (P0) | State |
|
||||||
|
| `Step4Report.vue` (modified) | Frontend / step component | Report renderer; parses backend report markdown | 2.2, 2.4, 2.6, 5.1–5.11 | `vue-i18n` (P0); `backend/app/services/zep_tools.py` markers (P0, read-only) | State |
|
||||||
|
| `Step5Interaction.vue` (modified) | Frontend / step component | Chat / interview UI | 2.3, 2.4, 2.6 | `vue-i18n` (P0) | State |
|
||||||
|
| `locales/en.json`, `locales/zh.json` | Frontend / data | Translation source; structurally aligned | 3.1–3.5 | — | Data |
|
||||||
|
| `audit-i18n-strings.sh` | Frontend / tooling | Local verification of remaining CJK literals across the 5 files | 6.1–6.4 | bash 5 + ripgrep + jq (P0) | CLI |
|
||||||
|
|
||||||
|
### Frontend / Step component
|
||||||
|
|
||||||
|
#### `Step4Report.vue` — `REPORT_MARKERS` constants block
|
||||||
|
|
||||||
|
| Field | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| Intent | Centralise the 29 backend-coupled Chinese marker strings/regexes into a single top-of-file constant block, so future backend translation requires a single-file edit. |
|
||||||
|
| Requirements | 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 5.10, 5.11 |
|
||||||
|
|
||||||
|
**Responsibilities & Constraints**
|
||||||
|
|
||||||
|
- The constants block owns every regex that pattern-matches against backend-emitted markdown structure (section headers, counter lines, interview fields, no-reply markers, search-query markers, log-severity tokens).
|
||||||
|
- Each constant carries an inline comment naming the canonical backend source line in `zep_tools.py` so future maintainers can find it.
|
||||||
|
- Each constant exposes either `.regex` (a `RegExp`) or `.is(value)` (an exact-match predicate).
|
||||||
|
- For markers whose translated wording is decided (today: only `noReply`'s English equivalent if/when it lands), use a single `RegExp` with alternation: `/(?:(该平台未获得回复)|\(该平台未获得回复\)|\[无回复\])/`. For markers whose translated wording is undecided, encode the Chinese form only and document the backend coordination point.
|
||||||
|
- The constants block does **not** import anything from `vue-i18n` — these are *backend-coupled patterns*, not user-visible strings.
|
||||||
|
|
||||||
|
**Dependencies**
|
||||||
|
|
||||||
|
- Inbound: every parser function in `Step4Report.vue` (P0)
|
||||||
|
- Outbound: none
|
||||||
|
- External: read-only knowledge of backend-emitted markers in `backend/app/services/zep_tools.py:47-1720` (P0)
|
||||||
|
|
||||||
|
**Contracts**: State [x]
|
||||||
|
|
||||||
|
##### State Management
|
||||||
|
|
||||||
|
```js
|
||||||
|
// In <script setup>:
|
||||||
|
const REPORT_MARKERS = Object.freeze({
|
||||||
|
// backend: zep_tools.py:175 — f"分析问题: {self.query}"
|
||||||
|
analysisQuery: { regex: /分析问题:\s*(.+?)(?:\n|$)/ },
|
||||||
|
// backend: zep_tools.py:176 — f"预测场景: {self.simulation_requirement}"
|
||||||
|
predictionScene: { regex: /预测场景:\s*(.+?)(?:\n|$)/ },
|
||||||
|
// backend: zep_tools.py:178 — f"- 相关预测事实: {self.total_facts}条"
|
||||||
|
factsCount: { regex: /相关预测事实:\s*(\d+)/ },
|
||||||
|
entitiesCount: { regex: /涉及实体:\s*(\d+)/ },
|
||||||
|
relationsCount: { regex: /关系链:\s*(\d+)/ },
|
||||||
|
subQueriesHeader: { regex: /### 分析的子问题\n/ },
|
||||||
|
keyFactsHeader: { regex: /### 【关键事实】/ },
|
||||||
|
coreEntitiesHdr: { regex: /### 【核心实体】\n/ },
|
||||||
|
entitySummary: { regex: /摘要:\s*"?(.+?)"?(?:\n|$)/ },
|
||||||
|
relatedFactsCnt: { regex: /相关事实:\s*(\d+)/ },
|
||||||
|
relationChainHdr: { regex: /### 【关系链】\n/ },
|
||||||
|
activeFactsCnt: { regex: /当前有效事实:\s*(\d+)/ },
|
||||||
|
activeFactsHdr: { regex: /### 【当前有效事实】/ },
|
||||||
|
historicalHdr: { regex: /### 【历史\/过期事实】/ },
|
||||||
|
involvedEntities: { regex: /### 【涉及实体】\n/ },
|
||||||
|
interviewTopic: { regex: /\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/ },
|
||||||
|
interviewCount: { regex: /\*\*采访人数:\*\*\s*(\d+)\s*\/\s*(\d+)/ },
|
||||||
|
selectionReason: { regex: /### 采访对象选择理由\n/ },
|
||||||
|
agentBio: { regex: /_简介:\s*([\s\S]*?)_\n/ },
|
||||||
|
twitterAnswer: { regex: /【Twitter平台回答】\n?/ },
|
||||||
|
redditAnswer: { regex: /【Reddit平台回答】\n?/ },
|
||||||
|
keyQuotesHeader: { regex: /\*\*关键引言:\*\*\n/ },
|
||||||
|
interviewSummary: { regex: /### 采访摘要与核心观点\n/ },
|
||||||
|
searchQuery: { regex: /搜索查询:\s*(.+?)(?:\n|$)/ },
|
||||||
|
relatedFactsHdr: { regex: /### 相关事实:\n/ },
|
||||||
|
relatedEdgesHdr: { regex: /### 相关边:\n/ },
|
||||||
|
relatedNodesHdr: { regex: /### 相关节点:\n/ },
|
||||||
|
// No-reply marker — checked as exact-match predicate, not a regex.
|
||||||
|
// backend: zep_tools.py:1424-1425
|
||||||
|
noReply: {
|
||||||
|
is(value) {
|
||||||
|
return value === '(该平台未获得回复)'
|
||||||
|
|| value === '(该平台未获得回复)'
|
||||||
|
|| value === '[无回复]'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Log severity classification — backend logs may interleave English and Chinese severity
|
||||||
|
// markers (e.g. Python's logging emits 'ERROR' but legacy logs include '错误'/'警告').
|
||||||
|
// Kept bilingual; deliberate per Requirement 5.11.
|
||||||
|
logSeverity: {
|
||||||
|
isError(line) { return line.includes('ERROR') || line.includes('错误') },
|
||||||
|
isWarning(line) { return line.includes('WARNING') || line.includes('警告') },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Preconditions**: `REPORT_MARKERS` is constructed once at module-evaluation time (`Object.freeze` to prevent accidental mutation).
|
||||||
|
- **Postconditions**: every parser function in the file references this object instead of literal regex/string. The diff for each parser is mechanical (`text.match(/分析问题:\s*…/) → text.match(REPORT_MARKERS.analysisQuery.regex)`).
|
||||||
|
- **Invariants**: the file contains no Chinese regex literals outside this block. The audit allowlist (`audit-i18n-strings.sh`) explicitly accepts the literals inside this block; any new Chinese literal added elsewhere in the file is flagged.
|
||||||
|
|
||||||
|
**Implementation Notes**
|
||||||
|
|
||||||
|
- Integration: replace each call site one at a time; a quick `npm run dev` smoke test (open a finished project, view its report, scroll through interviews and search results) confirms parity. No unit tests are added — the project's testing posture is "exercise the feature in a browser" per `tech.md`.
|
||||||
|
- Validation: a sentinel regex for the audit (e.g. `/[一-鿿]/u`) confirms zero CJK literals outside the allowlist.
|
||||||
|
- Risks: missing a parser site → silent regression. Mitigation: audit script catches any inline Chinese regex; manual smoke test confirms a real report renders identically.
|
||||||
|
|
||||||
|
#### `Step2EnvSetup.vue` — `STAGE_PHASE_MAP`
|
||||||
|
|
||||||
|
| Field | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| Intent | Replace three string-equality checks with a single source-of-truth lookup, accepting Chinese, snake_case, and future English stage names. |
|
||||||
|
| Requirements | 4.1, 4.2, 4.3, 4.4, 2.5, 2.6 |
|
||||||
|
|
||||||
|
**State Management**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const STAGE_PHASE_MAP = Object.freeze({
|
||||||
|
'生成Agent人设': 1,
|
||||||
|
'generating_profiles': 1,
|
||||||
|
'生成模拟配置': 2,
|
||||||
|
'generating_config': 2,
|
||||||
|
'准备模拟脚本': 2,
|
||||||
|
'copying_scripts': 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(currentStage, (newStage, oldStage) => {
|
||||||
|
const newPhase = STAGE_PHASE_MAP[newStage]
|
||||||
|
if (newPhase === undefined) return
|
||||||
|
phase.value = newPhase
|
||||||
|
if (newPhase === 2 && STAGE_PHASE_MAP[oldStage] !== 2 && !configTimer) {
|
||||||
|
addLog(t('log.startGeneratingConfig'))
|
||||||
|
startConfigPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Preconditions**: `currentStage` is a string emitted by the backend stage tracker.
|
||||||
|
- **Postconditions**: `phase.value` updates exactly when `currentStage` is a recognised stage name; otherwise unchanged.
|
||||||
|
- **Invariants**: adding a new stage requires only adding a row to `STAGE_PHASE_MAP`; no other change to `Step2EnvSetup.vue`.
|
||||||
|
|
||||||
|
### Frontend / data
|
||||||
|
|
||||||
|
#### `locales/en.json`, `locales/zh.json` — new keys
|
||||||
|
|
||||||
|
The new keys are listed below as a single design contract. Implementation sequences them by file (Process → Step3 → Step4 → Step5) to keep PR review chunks reviewable.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"process": {
|
||||||
|
"buildTitle": "Graph Build / 图谱构建",
|
||||||
|
"graphPanelTitle": "Real-time Knowledge Graph / 实时知识图谱",
|
||||||
|
"nodes": "nodes / 节点",
|
||||||
|
"edges": "relations / 关系",
|
||||||
|
"refreshGraph": "Refresh graph / 刷新图谱",
|
||||||
|
"exitFullscreen": "Exit fullscreen / 退出全屏",
|
||||||
|
"enterFullscreen": "Fullscreen / 全屏显示",
|
||||||
|
"realtimeUpdating": "Updating in real time… / 实时更新中…",
|
||||||
|
"graphLoading": "Loading graph data… / 图谱数据加载中…",
|
||||||
|
"waitingOntology": "Waiting for ontology generation / 等待本体生成",
|
||||||
|
"waitingOntologyHint": "Graph build will start automatically once ontology generation completes / 生成完成后将自动开始构建图谱",
|
||||||
|
"graphBuildingTitle": "Graph build in progress / 图谱构建中",
|
||||||
|
"graphBuildingHint": "Data will appear shortly… / 数据即将显示…",
|
||||||
|
"buildFlow": "Build flow / 构建流程",
|
||||||
|
"ontologyGeneration": "Ontology generation / 本体生成",
|
||||||
|
"interfaceNote": "Interface / 接口说明",
|
||||||
|
"ontologyDescription": "Upload documents; the LLM analyses them and automatically generates an ontology (entity types + relation types) suitable for opinion simulation / 上传文档后,LLM分析文档内容,自动生成适合舆论模拟的本体结构(实体类型 + 关系类型)",
|
||||||
|
"generationProgress": "Generation progress / 生成进度",
|
||||||
|
"generatedEntityTypes": "Generated entity types / 生成的实体类型",
|
||||||
|
"generatedRelationTypes": "Generated relation types / 生成的关系类型",
|
||||||
|
"waitingOntologyShort": "Waiting for ontology generation… / 等待本体生成…",
|
||||||
|
"graphBuildSection": "Graph build / 图谱构建",
|
||||||
|
"graphBuildDescription": "Using the generated ontology, chunks the documents and calls the Zep API to build the knowledge graph (entities + relations) / 基于生成的本体,将文档分块后调用 Zep API 构建知识图谱,提取实体和关系",
|
||||||
|
"waitingOntologyComplete": "Waiting for ontology generation to finish… / 等待本体生成完成…",
|
||||||
|
"entityNodes": "Entity nodes / 实体节点",
|
||||||
|
"relationEdges": "Relation edges / 关系边",
|
||||||
|
"entityTypes": "Entity types / 实体类型",
|
||||||
|
"buildComplete": "Build complete / 构建完成",
|
||||||
|
"buildCompleteHint": "Ready to proceed to the next step / 准备进入下一步骤",
|
||||||
|
"enterEnvSetup": "Enter environment setup / 进入环境搭建",
|
||||||
|
"projectInfo": "Project info / 项目信息",
|
||||||
|
"projectName": "Project name / 项目名称",
|
||||||
|
"projectId": "Project ID / 项目ID",
|
||||||
|
"graphId": "Graph ID / 图谱ID",
|
||||||
|
"simulationRequirement": "Simulation requirement / 模拟需求",
|
||||||
|
"buildFailed": "Build failed / 构建失败",
|
||||||
|
"buildSuccess": "Build complete / 构建完成",
|
||||||
|
"buildInProgress": "Graph build in progress / 图谱构建中",
|
||||||
|
"ontologyInProgress": "Ontology generation in progress / 本体生成中",
|
||||||
|
"buildInitializing": "Initializing… / 初始化中",
|
||||||
|
"envSetupComingSoon": "Environment setup feature coming soon… / 环境搭建功能开发中…",
|
||||||
|
"stepCompleted": "Completed / 已完成",
|
||||||
|
"stepInProgress": "In progress / 进行中",
|
||||||
|
"stepWaiting": "Waiting / 等待中",
|
||||||
|
"noFilesError": "No pending uploads. Please return to the home page and start over. / 没有待上传的文件,请返回首页重新操作",
|
||||||
|
"uploadingFiles": "Uploading files and analysing documents… / 正在上传文件并分析文档...",
|
||||||
|
"ontologyGenerationFailed": "Ontology generation failed / 本体生成失败",
|
||||||
|
"projectInitFailedPrefix": "Project initialization failed: / 项目初始化失败: ",
|
||||||
|
"loadProjectFailed": "Failed to load project / 加载项目失败",
|
||||||
|
"loadProjectFailedPrefix": "Failed to load project: / 加载项目失败: ",
|
||||||
|
"processingFailed": "Processing failed / 处理失败",
|
||||||
|
"graphBuildStarting": "Starting graph build… / 正在启动图谱构建...",
|
||||||
|
"graphBuildTaskStarted": "Graph build task started… / 图谱构建任务已启动...",
|
||||||
|
"graphBuildStartFailed": "Failed to start graph build / 启动图谱构建失败",
|
||||||
|
"graphBuildStartFailedPrefix": "Failed to start graph build: / 启动图谱构建失败: ",
|
||||||
|
"graphProcessing": "Processing… / 处理中...",
|
||||||
|
"graphBuildComplete": "Build complete; loading graph… / 构建完成,正在加载图谱...",
|
||||||
|
"graphBuildFailedPrefix": "Graph build failed: / 图谱构建失败: ",
|
||||||
|
"waitingGraphData": "Waiting for graph data… / 等待图谱数据...",
|
||||||
|
"fallbackNodeName": "Unnamed / 未命名"
|
||||||
|
},
|
||||||
|
"step3": {
|
||||||
|
"startFailed": "Failed to start / 启动失败"
|
||||||
|
},
|
||||||
|
"step4": {
|
||||||
|
"selectionReason": "Selection reason / 选择理由",
|
||||||
|
"awaitingStart": "Awaiting start / 等待开始"
|
||||||
|
},
|
||||||
|
"step5": {
|
||||||
|
"chatRolePrompter": "Questioner / 提问者",
|
||||||
|
"chatRoleYou": "You / 你",
|
||||||
|
"chatHistoryPrefix": "Here is our previous conversation:\n{history}\n\nMy new question is: {message} / 以下是我们之前的对话:\n{history}\n\n现在我的新问题是:{message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: the **above table** is a *content sketch* — the actual `en.json` and `zh.json` entries each carry their own language only (no "/ 中文" sidecars). The table format is purely for reviewer convenience; the implementation tasks split it into language-specific entries.
|
||||||
|
|
||||||
|
### Frontend / tooling
|
||||||
|
|
||||||
|
#### `frontend/scripts/audit-i18n-strings.sh`
|
||||||
|
|
||||||
|
| Field | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| Intent | Local verifier; greps the five files for non-allowlisted CJK literals and confirms en.json/zh.json key parity. |
|
||||||
|
| Requirements | 6.1, 6.2, 6.3, 6.4 |
|
||||||
|
|
||||||
|
**Responsibilities & Constraints**
|
||||||
|
|
||||||
|
- Greps a regex matching CJK code points (`[一-鿿 -〿-]`) over the five files only.
|
||||||
|
- Filters out lines whose grep match is fully contained in the allowlist (the `REPORT_MARKERS` constants block range, the bilingual log-severity helper, the i18n message tables in `locales/`, and any other deliberate token annotated with a `// i18n-allow:<reason>` trailing comment).
|
||||||
|
- Runs `jq -S 'paths(scalars) | join(".")' locales/en.json | sort -u` against the same for `zh.json` and diffs the result; reports missing keys.
|
||||||
|
- Exits 0 on success, 1 with a human-readable list on failure.
|
||||||
|
|
||||||
|
**Dependencies**
|
||||||
|
|
||||||
|
- External: bash 5+, ripgrep, jq.
|
||||||
|
|
||||||
|
**Contracts**: CLI [x]
|
||||||
|
|
||||||
|
##### CLI Contract
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ bash frontend/scripts/audit-i18n-strings.sh
|
||||||
|
# (no output) → exit 0
|
||||||
|
# OR
|
||||||
|
# frontend/src/views/Process.vue:42: <some Chinese literal>
|
||||||
|
# locale parity: missing in en.json: foo.bar
|
||||||
|
# exit 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation Notes**
|
||||||
|
|
||||||
|
- Integration: invoked manually before opening a PR. Not added to CI by this spec (out of boundary).
|
||||||
|
- Validation: dogfooded by running it before and after the file changes — the diff between the two runs is the spec's progress meter.
|
||||||
|
- Risks: the allowlist mechanism could be abused. Mitigation: keep the allowlist short and explicit (no glob patterns; the exact literal strings only).
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### Logical Data Model
|
||||||
|
|
||||||
|
The only "data" introduced by this spec is the new entries in `locales/en.json` and `locales/zh.json`. Their structure follows the existing namespaced JSON shape (`{ namespace: { key: "value", … } }`). No schema changes; no new namespaces beyond `process.*`.
|
||||||
|
|
||||||
|
### Data Contracts & Integration
|
||||||
|
|
||||||
|
**API Data Transfer**: none.
|
||||||
|
|
||||||
|
**Event Schemas**: none.
|
||||||
|
|
||||||
|
**Cross-Service Data Management**: none.
|
||||||
|
|
||||||
|
The only cross-component "data contract" is the implicit shape of `REPORT_MARKERS` (each entry exposes either `.regex` or `.is(...)`). It is private to `Step4Report.vue`.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Strategy
|
||||||
|
|
||||||
|
This spec is presentation-layer only; there is no new error path. Existing error paths are preserved:
|
||||||
|
|
||||||
|
- A missing translation key falls back through `vue-i18n`'s built-in fallback chain to the `'zh'` fallback locale; if the key is missing in both files, `vue-i18n` returns the key string itself. The audit script catches the structural divergence before runtime.
|
||||||
|
- A backend marker that doesn't match any `REPORT_MARKERS.*` regex returns `null` from the parser, which the existing rendering logic handles (renders the section as-is or skips it). No new error class.
|
||||||
|
|
||||||
|
### Error Categories and Responses
|
||||||
|
|
||||||
|
- **User Errors**: n/a.
|
||||||
|
- **System Errors**: n/a.
|
||||||
|
- **Business Logic Errors**: an unrecognised stage name in `STAGE_PHASE_MAP` is silently ignored (today's behaviour). The watcher does not throw.
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
No new logs. The existing `addLog(t('log.startGeneratingConfig'))` call is preserved.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
The project's testing posture is "exercise the feature in a browser" (per `tech.md`). The implementation tasks include:
|
||||||
|
|
||||||
|
- **Smoke tests (manual)**:
|
||||||
|
1. Switch to `en` locale; load a finished project; walk Step1 → Step5; visually confirm no Chinese in DOM (excluding backend-emitted content currently in Chinese).
|
||||||
|
2. Switch to `zh` locale; same walkthrough; visually confirm everything matches the production Chinese text.
|
||||||
|
3. Open a finished project's report; scroll through key facts, core entities, relation chains, sub-queries, interview answers (Twitter + Reddit), and search results. Confirm parity with `main` branch behaviour.
|
||||||
|
4. Trigger the chat flow in Step5 with one or two follow-up messages on each locale; confirm the prompt strings the LLM sees are well-formed.
|
||||||
|
|
||||||
|
- **Audit verification**:
|
||||||
|
1. Run `bash frontend/scripts/audit-i18n-strings.sh`; expect exit 0 and no output.
|
||||||
|
2. Run `wc -l locales/en.json locales/zh.json`; expect equal line counts (pre-#20 invariant maintained).
|
||||||
|
|
||||||
|
- **Regression (automated)**: none added — the project doesn't have a frontend test harness, and adding one is explicitly an out-of-scope decision per `tech.md`.
|
||||||
|
|
||||||
|
## Optional Sections
|
||||||
|
|
||||||
|
(Security, performance, scalability, migration are not relevant to this spec.)
|
||||||
|
|
||||||
|
## Supporting References
|
||||||
|
|
||||||
|
- `research.md` — discovery findings, design decisions, risk register.
|
||||||
|
- `requirements.md` — EARS-format acceptance criteria.
|
||||||
|
- `backend/app/services/zep_tools.py:47-1720` — canonical source of every backend marker the frontend parses.
|
||||||
|
- `frontend/src/i18n/index.js` — the `vue-i18n` instance configuration (no changes).
|
||||||
|
- Issue #25 / `.kiro/specs/i18n-report-agent-prompts/` — the downstream backend prompt translation; future trigger for editing `REPORT_MARKERS`.
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Gap Analysis — `i18n-frontend-ui-strings`
|
||||||
|
|
||||||
|
## 1. Scope and current state
|
||||||
|
|
||||||
|
The five files in scope are at very different stages of i18n adoption. The audit drilled into each one and uncovered substantially more flagged sites than the ticket body enumerated; the spec is broader in practice than its evidence list suggests.
|
||||||
|
|
||||||
|
| File | i18n adoption today | Touch surface |
|
||||||
|
| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| `views/Process.vue` | **None**. No `useI18n()` import, no `$t` calls. ~40 user-visible Chinese literals across template + JS. | All template headers, status badges, modal/info labels, error strings, fallback names. |
|
||||||
|
| `components/Step2EnvSetup.vue` | High (~70%). Templates already use `$t`. Remaining: 3 backend-stage equality checks (lines 680/682/689) and a few `console.warn` strings (not user-visible). | Stage-watcher logic only. |
|
||||||
|
| `components/Step3Simulation.vue` | Sparse (1 `$t` call). The template is mostly English already, but the JS has `startError.value = res.error \|\| '启动失败'` (line 423/427). | A single fallback string. |
|
||||||
|
| `components/Step4Report.vue` | Sparse (2 `$t` calls). The bulk of the file is **29 regex patterns** matching Chinese section markers emitted by `report_agent.py`, plus three string-equality checks for the no-reply marker, plus a log-severity classifier. | Parser/regex layer + a small set of UI literals (e.g. line 1464 `'选择理由'`, line 1774 `'等待开始'`). |
|
||||||
|
| `components/Step5Interaction.vue` | Extensive (~35 `$t` calls). Two literals remain: lines 725 and 727 — both **prompt strings sent to the backend LLM**, not user-visible UI. | Prompt construction in the chat-history feature. |
|
||||||
|
|
||||||
|
`createI18n` lives at `frontend/src/i18n/index.js`. Default locale is `'zh'` (read from `localStorage`), fallback `'zh'`. Locales come from repo-root `/locales/*.json` via `import.meta.glob`. Both `en.json` and `zh.json` are aligned at 1031 lines after issue #20's backfill.
|
||||||
|
|
||||||
|
The audit script the original ticket refers to (`.kiro/specs/i18n-e2e-english-verification/audit/scripts/run_audit.sh`) **does not exist** — only the empty directory shell remains. Verification has to be done by direct grep over the five files (Requirement 6).
|
||||||
|
|
||||||
|
## 2. Requirement-to-asset map
|
||||||
|
|
||||||
|
| Req | Need | Existing asset (file) | Gap label |
|
||||||
|
| --- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||||
|
| R1 | Wire `useI18n()` into `Process.vue`; route ~40 strings through new `process.*` keys | No i18n import; no `process.*` namespace yet | **Missing** — both wiring and namespace |
|
||||||
|
| R2 | Externalize remaining UI literals in Step3/Step4/Step5 | `useI18n()` already imported in all three; namespaces `step3/4/5.*` exist | **Constraint** — match existing namespace structure |
|
||||||
|
| R3 | en.json/zh.json parity for new keys | Issue #20 / spec `i18n-backfill-zh-json` already aligned them | **Constraint** — must not regress; checker exists informally |
|
||||||
|
| R4 | Stage-name comparator that survives backend translation | `Step2EnvSetup.vue:679-692` watcher; backend emits both Chinese display strings and snake_case ids | **Missing** — small lookup map needed |
|
||||||
|
| R5 | Bilingual (Chinese + English) tolerance for 29 regex parsers + 3 marker checks + log classifier | `Step4Report.vue:557-943` regex block; `:1334`, `:2014-2015` checks | **Missing** — backend-coupled; depends on what translated prompts emit |
|
||||||
|
| R6 | Local verification check | Audit script gone; vanilla `grep` available | **Missing** — small script or documented one-liner |
|
||||||
|
|
||||||
|
### Research-needed flags
|
||||||
|
|
||||||
|
- **Backend-emitted English strings** (R5) — issues #25 (LLM prompt assembly translation) and the open `i18n-report-agent-prompts` spec dictate what English markers `report_agent.py` will eventually emit (e.g., will it become `Analysis question:`, `Analyze question:`, or stay as a stable `分析问题:`?). The frontend can't pin its English regexes to specific wording until that backend spec is settled. **Mitigation strategy**: keep the existing Chinese regexes intact (for backwards compat with current backend) and add deliberately permissive English alternates that match a documented set of likely renderings, plus keep an explicit allowlist of "deliberate Chinese tokens for backend compatibility" so the audit doesn't re-flag them.
|
||||||
|
- **Step5Interaction.vue prompt strings** (lines 725, 727) — these are not UI; they are *prompt content sent to the LLM*. If the user is on `zh` locale and chats with a Chinese-trained agent, the Chinese phrasing is intentional. Two viable strategies: (a) localize to active locale via `t()` (correct linguistically; assumes the agent handles both), or (b) keep Chinese literally because the agent is currently Chinese-tuned. The wider initiative is moving the backend to English, so option (a) aligns better — but coordinating with the report agent's actual prompt translation (separate spec) would be ideal. For this spec, route through i18n keys and let the active locale dictate; document the assumption.
|
||||||
|
|
||||||
|
## 3. Implementation approach options
|
||||||
|
|
||||||
|
### Option A — Pure extension (recommended)
|
||||||
|
|
||||||
|
Treat this as a localized cleanup of five existing files plus locale-file additions. All work is "match the surrounding file's style," no new abstractions.
|
||||||
|
|
||||||
|
- **Files extended**: 5 Vue files + 2 locale files. No new files (or 1 new file: a small grep-based verifier).
|
||||||
|
- **Compatibility**: The new i18n keys layer onto an existing pattern (`step2.*`, `step4.*`, etc.). No public surface changes.
|
||||||
|
- **Complexity**: Low to medium. The big number is the count of strings, not their individual difficulty. The two genuinely tricky pieces are (i) the bilingual regex strategy in `Step4Report.vue`, (ii) the stage-comparator refactor in `Step2EnvSetup.vue`.
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
|
||||||
|
- ✅ Least architectural change; matches the steering principle "match the surrounding file's style."
|
||||||
|
- ✅ Each file's edits are independent; reviewable in isolation.
|
||||||
|
- ❌ Adds size to `Process.vue` (already ~50KB) — but the additions are mostly `t()` substitutions, not new logic.
|
||||||
|
|
||||||
|
### Option B — Extract a `useStageMatcher()` composable + a `parseReportSection()` utility
|
||||||
|
|
||||||
|
Make new files for the two backend-coupled responsibilities (R4 and R5):
|
||||||
|
|
||||||
|
- `frontend/src/composables/useStageMatcher.js` — owns the legacy-Chinese ↔ snake_case ↔ future-English equivalence map.
|
||||||
|
- `frontend/src/utils/reportParsers.js` — owns the bilingual regex set and exposes typed extractors.
|
||||||
|
|
||||||
|
**Trade-offs**:
|
||||||
|
|
||||||
|
- ✅ Cleaner separation; future backend-output changes localize into one parser file.
|
||||||
|
- ❌ Moves regexes out of `Step4Report.vue` where today they sit alongside the consumer; the steering doc favors keeping pipeline-stage logic in the matching Step component unless responsibilities truly diverge. Arguably they don't yet.
|
||||||
|
- ❌ Larger PR, more files for reviewers to navigate.
|
||||||
|
|
||||||
|
### Option C — Hybrid
|
||||||
|
|
||||||
|
Apply Option A everywhere, but extract `useStageMatcher()` (Option B's smaller half) because the stage equality check is duplicated three times in one watcher and obviously benefits from a single source of truth. Leave the `Step4Report.vue` parsers in place and add the bilingual alternates inline; this avoids designing an extraction boundary against a backend that's still being translated.
|
||||||
|
|
||||||
|
## 4. Recommendation
|
||||||
|
|
||||||
|
**Hybrid (Option C)**.
|
||||||
|
|
||||||
|
- **R1, R2, R3**: pure extension — substitute `t('…')` and add keys to both locale files.
|
||||||
|
- **R4**: introduce a tiny in-file lookup (or, if it earns its keep, a small composable) so that future stage strings only need adding in one place.
|
||||||
|
- **R5**: extend the existing regexes inline. Each parser becomes `chineseRegex.test(...) ? extract(chineseRegex) : englishRegex.exec(...)` (or an explicitly bilingual single regex like `/(?:分析问题|Analysis question):/`). Document the deliberate Chinese tokens at the top of the parser block so the verifier in R6 can allowlist them.
|
||||||
|
- **R6**: a `frontend/scripts/audit-i18n-strings.{sh,js}` (the existing repo has no equivalent yet) that greps the five files for unicode CJK literals minus an allowlist; runnable locally in seconds, no backend required. Keep it tiny — this is verification, not a new test framework.
|
||||||
|
|
||||||
|
**Effort**: **M** (3–7 days of focused work). The volume in `Process.vue` is significant (~40 keys), but each substitution is mechanical. R5 is the only piece with real design content.
|
||||||
|
|
||||||
|
**Risk**: **Medium**. Risk concentration:
|
||||||
|
|
||||||
|
- **R5 (parsers)** — Medium. If the backend prompt translations land between this PR's merge and a release, and the actual English wording doesn't match the alternates we encoded, reports degrade silently. Mitigation: write the alternates against the actual prompts in `backend/app/services/report_agent.py` rather than guessing, and keep all Chinese regexes alive as fallbacks for as long as the backend is partially-translated.
|
||||||
|
- **R3 (locale parity)** — Low. Tooling-aided; #20 just established the discipline.
|
||||||
|
- **R1 (Process.vue)** — Low individually, but the volume means review fatigue is a real risk. Recommend the PR call out logical chunks (header / progress / errors / fallback labels) so the reviewer can sign off section by section.
|
||||||
|
|
||||||
|
## 5. Research items to carry into design
|
||||||
|
|
||||||
|
1. Read `backend/app/services/report_agent.py` (and any prompt template referenced by it) to enumerate the *actual* English strings it emits *today*, not what we assume it will emit. The R5 regex alternates must be grounded in real backend output. Cross-reference the open spec `i18n-report-agent-prompts` for the planned English wording.
|
||||||
|
2. Confirm whether `Step2EnvSetup.vue:680-689` ever sees backend-emitted English display strings now, or only the snake_case identifiers. If only snake_case, the comparator can prefer those and treat the Chinese as backwards-compat-only.
|
||||||
|
3. Confirm that `Step5Interaction.vue:725-727` (chat history templating) is acceptable to localize via `t()` — i.e., that the report agent handles both `Question: …` and `提问者:…` framings of chat history. If not, leave Chinese in for now and open a separate ticket to migrate alongside the backend agent prompt translation.
|
||||||
|
4. Decide naming for the new `process.*` namespace: align with adjacent step namespaces (`step1.*` etc.) or use a fresher grouping. The existing `graph.*` namespace already covers some of the graph-panel headers and may absorb several of `Process.vue`'s strings rather than duplicating them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output checklist
|
||||||
|
|
||||||
|
- ✅ Requirement-to-asset map with gaps tagged
|
||||||
|
- ✅ Options A/B/C with trade-offs
|
||||||
|
- ✅ Effort **M**, Risk **Medium** with justification
|
||||||
|
- ✅ Recommendation: Option C (hybrid)
|
||||||
|
- ✅ Research items carried forward
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Project Description (Input)
|
||||||
|
Replace hard-coded Chinese UI strings in frontend Vue components and views with vue-i18n keys, and update Step4Report.vue regex parsers that depend on Chinese tokens emitted by the backend so they keep working once those backend prompts are translated to English. Scope: frontend/src/components/Step2EnvSetup.vue, Step3Simulation.vue, Step4Report.vue, Step5Interaction.vue and frontend/src/views/Process.vue. The audit list lives at .kiro/specs/i18n-e2e-english-verification/audit/classified.csv. Acceptance: every flagged file:line is fixed (translated via i18n keys, or kept as deliberate) and the audit script .kiro/specs/i18n-e2e-english-verification/audit/scripts/run_audit.sh reports zero gaps in this category. Reference: GitHub issue #23.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Five frontend Vue files (`Process.vue`, `Step2EnvSetup.vue`, `Step3Simulation.vue`, `Step4Report.vue`, `Step5Interaction.vue`) still emit Chinese strings directly to the user instead of routing them through `vue-i18n`. The `Step4Report.vue` parsers also pattern-match against Chinese tokens emitted by backend prompts; once those backend prompts are translated as part of the wider i18n initiative, those parsers will silently fail. This spec scopes both fixes: externalise user-visible strings to `/locales/{en,zh}.json`, and make the report parsers tolerate the post-translation backend output.
|
||||||
|
|
||||||
|
This is a remediation slice of the broader i18n initiative tracked by epic #11.
|
||||||
|
|
||||||
|
## Boundary Context
|
||||||
|
|
||||||
|
- **In scope**:
|
||||||
|
- User-visible Chinese strings in templates, alert/message bodies, error fallbacks, and runtime messages inside the five files listed above.
|
||||||
|
- Comparison/branching logic that relies on Chinese stage tokens for stable behaviour (e.g., `currentStage === '生成Agent人设'`).
|
||||||
|
- Regex parsers and string-equality checks in `Step4Report.vue` and `Step5Interaction.vue` that depend on Chinese tokens emitted by the backend (`相关预测事实: X条`, `位模拟Agent`, `选择…(index …)`, `(该平台未获得回复)`, `[无回复]`, `问题X:`, `分析问题:`, `最终答案:`, `提问者`/`你`, `以下是我们之前的对话`, etc.).
|
||||||
|
- Adding any newly required keys to both `locales/en.json` and `locales/zh.json` so the existing parity is preserved.
|
||||||
|
- **Out of scope**:
|
||||||
|
- Translating backend log messages, ontology/report agent prompts, or other backend code (covered by issues #24, #25, and the existing per-prompt specs).
|
||||||
|
- Translating Chinese comments in source files (covered by issues #7 and #9).
|
||||||
|
- Re-running the audit script — the artefacts at `.kiro/specs/i18n-e2e-english-verification/audit/scripts/` no longer exist. Acceptance is verified by direct grep over the five files.
|
||||||
|
- Changes to other Vue components/views beyond the five named in scope, unless a shared key the five files use needs extending.
|
||||||
|
- **Adjacent expectations**:
|
||||||
|
- i18n key naming follows the conventions already in `/locales/en.json` (camelCase, namespaced by view/component, e.g. `step2.*`, `step4.*`, `process.*`).
|
||||||
|
- The frontend uses `vue-i18n` 11; the global `$t` is available in templates and `useI18n()`'s `t` is used in `<script setup>` blocks already in each file.
|
||||||
|
- Both Chinese (`zh.json`) and English (`en.json`) entries must be supplied for every new key — no English-only keys (this matches the resolution applied in #20 / spec `i18n-backfill-zh-json`).
|
||||||
|
- Backend prompts will be translated under separate specs; this spec must keep the parsers working both **before** and **after** that backend change, because the two changes will not land atomically.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: Externalize hard-coded UI strings in `Process.vue`
|
||||||
|
|
||||||
|
**Objective:** As an English-locale user, I want the workflow orchestrator page to render every label, status, button title, and error message in the language selected via the language switcher, so that I am not unexpectedly shown Chinese text in the middle of an otherwise English UI.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The `Process.vue` file shall render every user-visible string flagged in the ticket evidence (lines 26, 30, 32, 36, 39, 53, 452–456, 482, 536, 541, 543, 563, 571, 598, 602, 634, 638, 657, 667, 673, 681, 686, 763, 778, 797, 872, 884, 900, 901, plus their immediate Chinese-only siblings within those template/script blocks) through `vue-i18n` keys instead of inlined Chinese literals.
|
||||||
|
2. When the active locale is `en`, the `Process.vue` file shall render no Chinese characters in the rendered DOM for any path exercised on a clean project build (initial load, ontology generation, graph build start, graph build progress, graph build success, graph build error, refresh button, fullscreen toggle, fallback node/edge labels).
|
||||||
|
3. When the active locale is `zh`, the `Process.vue` file shall render the same Chinese wording the user sees today for every flagged string (no regression for Chinese users).
|
||||||
|
4. If a flagged string is a fallback for an entity name from the graph (`节点名 = n.name || '未命名'`), then the `Process.vue` file shall use a translated fallback (e.g., `t('process.fallbackNodeName')`) instead of the Chinese literal.
|
||||||
|
5. The `Process.vue` file shall not introduce any new English-only string literal in `<template>` or in user-visible script paths; every newly added literal shall be added to both `en.json` and `zh.json`.
|
||||||
|
|
||||||
|
### Requirement 2: Externalize hard-coded UI strings in step components
|
||||||
|
|
||||||
|
**Objective:** As an English-locale user, I want the Step2/Step3/Step4/Step5 components to surface every status badge, error toast, log line, modal copy, and inline label through i18n keys, so that no step of the pipeline silently shows Chinese to an English user.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The `Step3Simulation.vue` file shall route the simulation start-failure fallback (`'启动失败'`) through an i18n key (e.g. `t('step3.startFailed')`) so the message renders in the active locale.
|
||||||
|
2. The `Step4Report.vue` file shall route every user-visible Chinese literal flagged in the ticket evidence (lines 850, 854, 1325, 1464, 1774, 2005–2006, plus any equivalent literals discovered while editing those blocks) through i18n keys, including inline render-function strings (`h('div', …, '选择理由')`), placeholder titles (`'等待开始'`), the no-reply markers used in display branches, and log-classification labels.
|
||||||
|
3. The `Step5Interaction.vue` file shall route the chat-history templating (`'提问者'`, `'你'`, `'以下是我们之前的对话:…现在我的新问题是:…'`) through i18n keys so that the prompt sent to the backend reflects the active UI language, with the prior Chinese behaviour preserved when locale is `zh`.
|
||||||
|
4. When the active locale is `en`, the four step components in scope shall render no Chinese characters on any UI path that does not display backend-supplied content verbatim.
|
||||||
|
5. The `Step2EnvSetup.vue` file shall continue to track the simulation stage transitions for backend stage names whose Chinese form is currently observed (`'生成Agent人设'`, `'生成模拟配置'`, `'准备模拟脚本'`); see Requirement 4 for how this is preserved.
|
||||||
|
6. The four step components shall use the existing `useI18n()` `t` import already present in each file rather than introducing a different translation utility.
|
||||||
|
|
||||||
|
### Requirement 3: Maintain `en.json` and `zh.json` parity for newly externalized strings
|
||||||
|
|
||||||
|
**Objective:** As a maintainer, I want every new i18n key added by this work to exist in both the English and Chinese locale files with appropriate translations, so that neither locale ends up with English fallbacks shown to users (the regression that was just fixed in #20).
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The locale files shall contain an entry for every new key added under this spec, in both `locales/en.json` and `locales/zh.json`.
|
||||||
|
2. The `locales/zh.json` entries shall preserve the exact Chinese wording removed from the source files (no paraphrasing) so that the user-visible text in the Chinese UI is unchanged.
|
||||||
|
3. The `locales/en.json` entries shall contain idiomatic English translations consistent with the existing tone of the file (sentence case, no trailing punctuation unless the surrounding entries use it).
|
||||||
|
4. New keys shall be grouped under existing namespaces where one fits (e.g., `process.*`, `step2.*`, `step4.*`, `step5.*`); a new namespace shall only be introduced if no existing one covers the surface.
|
||||||
|
5. The two files shall remain structurally aligned (same set of keys, same nesting). Keys present in one locale but missing in the other shall be considered a defect.
|
||||||
|
|
||||||
|
### Requirement 4: Replace Chinese stage-name comparisons with stable language-independent identifiers
|
||||||
|
|
||||||
|
**Objective:** As a developer, I want the frontend's branching logic to rely on stable backend-emitted identifiers rather than Chinese display strings, so that translating those backend strings to English does not silently break stage transitions.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The `Step2EnvSetup.vue` watcher shall continue to enter the correct phase when the backend emits any of the stage forms currently observed in production: the legacy Chinese display strings (`'生成Agent人设'`, `'生成模拟配置'`, `'准备模拟脚本'`), the existing snake_case identifiers (`'generating_profiles'`, `'generating_config'`, `'copying_scripts'`), and any English display strings the backend may emit after its prompts are translated.
|
||||||
|
2. While the backend has not yet been translated, the `Step2EnvSetup.vue` file shall not regress its current behaviour for users on Chinese builds (the Chinese stage names must continue to map to the correct phase).
|
||||||
|
3. If the backend later removes the Chinese stage strings entirely, the `Step2EnvSetup.vue` file shall still drive `phase.value` correctly using the snake_case stage identifiers without any further frontend change.
|
||||||
|
4. The `Step2EnvSetup.vue` file shall encode the stage matching once (e.g., a small lookup) rather than scattering string equality checks, so that future stage additions only need to be made in one place.
|
||||||
|
|
||||||
|
### Requirement 5: Make `Step4Report.vue` parsers tolerate translated backend output
|
||||||
|
|
||||||
|
**Objective:** As a user running with translated backend prompts, I want the report renderer to extract counters, interview answers, persona titles, query strings, and final answers correctly even after the backend stops emitting Chinese, so that the report does not silently degrade once the i18n backend work lands.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. When the backend emits the legacy Chinese counter format (`相关预测事实: X条`), the `Step4Report.vue` file shall extract the counter value as it does today.
|
||||||
|
2. When the backend emits the equivalent English counter format produced by the translated prompts (e.g., `Related prediction facts: X` or whatever the translated prompt actually emits), the `Step4Report.vue` file shall extract the same counter value.
|
||||||
|
3. When the backend emits the legacy interview-count format (`5 / 9 位模拟Agent`), the `Step4Report.vue` file shall extract the numerator and denominator.
|
||||||
|
4. When the backend emits the equivalent English interview-count format (e.g., `5 / 9 simulated agents`), the `Step4Report.vue` file shall extract the same numerator and denominator.
|
||||||
|
5. When the backend emits a "no reply" marker — `(该平台未获得回复)`, `(该平台未获得回复)`, `[无回复]`, or the corresponding English markers produced by the translated prompts — the `Step4Report.vue` file shall recognise it as a no-reply value and suppress the empty bubble.
|
||||||
|
6. When the backend emits a numbered question label in the Chinese-style format (`问题X:` / `问题X:`) or its translated equivalent (e.g. `Question X:`), the `Step4Report.vue` file shall recognise both and split the prompt accordingly.
|
||||||
|
7. When the backend emits a "selection reason" line in the Chinese-style format (`- 选择<name>(index <i>):<reason>`) or its translated equivalent, the `Step4Report.vue` file shall recognise both and extract the same fields.
|
||||||
|
8. When the backend emits a `分析问题:` marker or the equivalent translated marker (e.g. `Analyse question:` / whatever the prompt produces), the `Step4Report.vue` file shall extract the query.
|
||||||
|
9. When the backend emits a `最终答案:` marker or its translated equivalent, the `Step4Report.vue` file shall extract the final answer body.
|
||||||
|
10. The `Step4Report.vue` file shall keep working when the backend has been only partially translated (some markers still Chinese, some English) — i.e., parsers shall accept either form, not require both.
|
||||||
|
11. The `Step4Report.vue` file shall keep its log-classification (`ERROR`/`错误`, `WARNING`/`警告`) functioning for both forms.
|
||||||
|
|
||||||
|
### Requirement 6: Verifiability
|
||||||
|
|
||||||
|
**Objective:** As a reviewer, I want a deterministic way to confirm that the five files no longer hard-code user-visible Chinese, so that I can sign off on the PR without manually walking every line.
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. The repository shall provide a check that, given the five files in scope, reports zero hard-coded user-visible Chinese strings (i.e., string literals containing CJK characters that flow into the rendered DOM, an `alert()`, or a backend prompt).
|
||||||
|
2. While the audit script referenced in the original ticket no longer exists in the repo, the spec shall document the equivalent check used to verify acceptance (e.g. a `grep` invocation scoped to the five files, with a small allowlist for translation-equivalence checks performed in Requirement 5).
|
||||||
|
3. If a Chinese literal is intentionally retained (e.g., to match a backend marker for backwards compatibility under Requirement 5), the `requirements.md` and the inline code shall identify it as deliberate, and the verification check shall not flag it.
|
||||||
|
4. The verification shall be runnable locally in under one minute and shall not require a running backend.
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Research & Design Decisions — `i18n-frontend-ui-strings`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Feature**: `i18n-frontend-ui-strings`
|
||||||
|
- **Discovery Scope**: Extension (existing Vue + vue-i18n adoption, brownfield codebase)
|
||||||
|
- **Key Findings**:
|
||||||
|
- `Process.vue` has zero i18n adoption today; the other four files in scope are partially i18n'd. The volume of new keys lands almost entirely in `Process.vue` and falls under the existing `process.*` / `graph.*` / `step1.*` namespaces.
|
||||||
|
- The 29 backend-coupled regexes in `Step4Report.vue` are matched against strings emitted by `backend/app/services/zep_tools.py` (and a few other services). Those backend strings are 100% Chinese today; the English translation of those prompts is owned by a *separate* spec (issue #25 / `i18n-report-agent-prompts` etc.) and has not landed.
|
||||||
|
- The `Step5Interaction.vue` chat-history templating (`'提问者'`/`'你'`/`'以下是我们之前的对话:…'`) is *prompt content* sent to the LLM, not user-visible UI. It is safe to localize via `t()` because the backend report agent already accepts both Chinese and English (it's a multilingual LLM) and the wider initiative is moving the backend to English; the Chinese phrasing is preserved for the `zh` locale via `zh.json`.
|
||||||
|
|
||||||
|
## Research Log
|
||||||
|
|
||||||
|
### Backend marker emission audit
|
||||||
|
|
||||||
|
- **Context**: Requirement 5 demands that `Step4Report.vue` parsers tolerate post-translation backend output. We need to know what the backend emits today and whether the translated wording is already pinned.
|
||||||
|
- **Sources Consulted**:
|
||||||
|
- `backend/app/services/zep_tools.py` (lines 47, 50, 78, 175–207, 258–276, 307–311, 379–395, 1365, 1424–1426, 1720)
|
||||||
|
- `backend/app/services/oasis_profile_generator.py` (lines 424–475, 945)
|
||||||
|
- GitHub issue #25 (open; backend prompt assembly translation not yet started)
|
||||||
|
- `.kiro/specs/i18n-report-agent-prompts/` (open; tasks generated, not implemented)
|
||||||
|
- **Findings**:
|
||||||
|
- All 29 markers/regexes used by the frontend originate from the listed backend files and are emitted as Chinese literals (e.g. `f"分析问题: {self.query}"`, `f"- 相关预测事实: {self.total_facts}条"`, `f"**采访人数:** {self.interviewed_count} / {self.total_agents} 位模拟Agent"`).
|
||||||
|
- The English wording the backend will emit after translation is **not yet pinned** in the open spec. It will be decided when `i18n-report-agent-prompts` and #25 are implemented.
|
||||||
|
- The no-reply markers (`(该平台未获得回复)`, `(该平台未获得回复)`, `[无回复]`) are also emitted by `zep_tools.py` as Chinese literals and used as user-visible "no answer" text in the report. These are the only markers whose translated wording we can reasonably anticipate (`(no response on this platform)` / `[no response]` etc.) and even those will be decided by the backend spec.
|
||||||
|
- **Implications**:
|
||||||
|
- We cannot reliably encode English alternates for markers whose translated wording is undecided. Speculative English regexes risk silently failing once the backend translation chooses a different wording.
|
||||||
|
- **Strategy**: centralize the markers in a single top-of-file constants block in `Step4Report.vue`, document them as "backend-coupled, deliberate Chinese — sync with `i18n-report-agent-prompts`", and surface them in the audit allowlist (Requirement 6). When the backend spec lands, a single-file edit updates every parser at once. This is the "deliberate" classification the ticket allows.
|
||||||
|
- The log-severity classifier (`log.includes('错误')` / `log.includes('警告')`) is a special case: those substrings come from arbitrary log lines, not a fixed marker. Keep them with bilingual `OR` (`'ERROR'/'错误'/'WARNING'/'警告'` is already what the file does). No change required beyond noting it as deliberate.
|
||||||
|
|
||||||
|
### vue-i18n usage convention
|
||||||
|
|
||||||
|
- **Context**: Confirm the adoption pattern so the new substitutions match existing files.
|
||||||
|
- **Sources Consulted**: `frontend/src/i18n/index.js`; the four step components already using i18n; `locales/en.json` (1031 lines), `locales/zh.json` (1031 lines, aligned post-#20).
|
||||||
|
- **Findings**:
|
||||||
|
- `<template>` uses `$t('namespace.key')` and `$t('namespace.key', { param })`; `<script setup>` uses `const { t } = useI18n()` then `t('namespace.key')`.
|
||||||
|
- Existing namespaces: `common`, `meta`, `nav`, `home`, `main`, `step1` … `step5`, `graph`, `history`, `api`, `progress`, `log`, `report`, `console`. No `process` namespace yet; the `graph.*` namespace already covers ~5 of `Process.vue`'s graph-panel strings (refresh/maximize/loading) and should absorb those.
|
||||||
|
- Default locale is `'zh'`; fallback locale is also `'zh'`. The frontend passes locale through `localStorage`. No SSR concerns.
|
||||||
|
- **Implications**:
|
||||||
|
- Add a new `process.*` namespace for view-level strings. Reuse `graph.*` (already covers refresh/maximize/etc.) for graph-panel literals where a key already exists.
|
||||||
|
- Add `step3.startFailed` (already exists — confirmed in en.json `step3.startFailed`), `step4.*` keys for the new Step4Report literals (`selectionReason`, `awaitingStart`, etc.), `step5.*` keys for the chat-history templating.
|
||||||
|
|
||||||
|
### Locale parity check
|
||||||
|
|
||||||
|
- **Context**: Issue #20 (`i18n-backfill-zh-json`) recently aligned `en.json` and `zh.json`. Don't regress that.
|
||||||
|
- **Sources Consulted**: `wc -l locales/{en,zh}.json` → both 1031.
|
||||||
|
- **Findings**: structurally aligned. Keys present in en.json are mirrored in zh.json. The discipline is fresh and respected.
|
||||||
|
- **Implications**: every new key added by this spec must land in *both* files in the same commit/PR. The existing audit script for parity is implicit — `diff <(jq -S 'keys_unsorted_recursive' en.json) <(jq -S 'keys_unsorted_recursive' zh.json)` is a one-liner that suffices. We will not add a CI gate (out of scope; tracked under issue #26).
|
||||||
|
|
||||||
|
## Architecture Pattern Evaluation
|
||||||
|
|
||||||
|
| Option | Description | Strengths | Risks / Limitations | Notes |
|
||||||
|
|--------|-------------|-----------|---------------------|-------|
|
||||||
|
| Pure extension (in-file `t()` substitution + add keys) | Match existing surrounding-file style; no new modules. | Smallest blast radius; reviewable file-by-file; aligns with steering "match the surrounding file's style." | Adds ~40 entries to each locale file; nothing centralized. | Picked for R1, R2, R3. |
|
||||||
|
| New `useStageMatcher` composable + `reportMarkers.js` util | Extract the two backend-coupled responsibilities (stage matching, parser markers) into shared modules. | Single source of truth; future backend translation is a one-line update. | More files; steering doc favours pipeline-stage logic *staying in* the matching Step component unless responsibilities truly diverge. They do diverge slightly (markers are coupled to backend, not to the Step's UI state). | Rejected for stage matcher (only 3 lines, not worth the extraction); chosen as in-file constants block for parser markers. |
|
||||||
|
| Hybrid (selected) | In-file `t()` substitution everywhere; in-file constants block for parser markers in `Step4Report.vue`; tiny in-file lookup for stage matcher in `Step2EnvSetup.vue`. | Balances clarity against the steering principle. Single edit when backend translation lands. | Constants block adds ~25 lines to `Step4Report.vue`. | **Selected**. |
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Decision: keep parser markers as in-file constants, not a separate utility
|
||||||
|
|
||||||
|
- **Context**: Requirement 5 wants the parsers to survive backend translation. The candidates for "where to put the marker definitions" are (a) inline in each parser, (b) top-of-file constants block, (c) separate `reportMarkers.js`.
|
||||||
|
- **Alternatives Considered**:
|
||||||
|
1. **Inline** — what the file does today. Rejected: 29 markers scattered across 400+ lines makes a future single-file update impossible.
|
||||||
|
2. **Separate utility module** — clean but pulls knowledge of report-agent output into a generic util that no other file uses. The steering doc's pipeline-aligned principle prefers staying in the Step component.
|
||||||
|
3. **Top-of-file constants block** — selected.
|
||||||
|
- **Selected Approach**: A `const REPORT_MARKERS = { … }` block at the top of `Step4Report.vue`'s `<script setup>`, with each entry documenting the backend source line. Each parser uses `REPORT_MARKERS.foo.regex` instead of inlining the literal regex. When `i18n-report-agent-prompts` lands, the block is the only place that needs editing.
|
||||||
|
- **Rationale**: minimal architectural change; preserves the steering principle that pipeline-stage logic lives in the matching Step component; gives R5 a single defensible edit-site for the future backend update.
|
||||||
|
- **Trade-offs**: 25 extra lines in an already-large file (+0.06% of `Step4Report.vue`). Acceptable.
|
||||||
|
- **Follow-up**: when issue #25 / `i18n-report-agent-prompts` lands, edit `REPORT_MARKERS` to alternate Chinese/English forms.
|
||||||
|
|
||||||
|
### Decision: stage-matcher refactor, not stage-matcher extraction
|
||||||
|
|
||||||
|
- **Context**: `Step2EnvSetup.vue:680-689` has three string-equality checks (`'生成Agent人设'`, `'生成模拟配置'`, `'准备模拟脚本'`).
|
||||||
|
- **Alternatives Considered**:
|
||||||
|
1. Leave alone — fails R4.
|
||||||
|
2. Extract `useStageMatcher()` composable — over-engineered for 3 entries.
|
||||||
|
3. Inline lookup map at top of file — selected.
|
||||||
|
- **Selected Approach**: a `const STAGE_PHASE_MAP = { 'generating_profiles': 1, '生成Agent人设': 1, 'generating_config': 2, '生成模拟配置': 2, 'copying_scripts': 2, '准备模拟脚本': 2 }`. Watcher becomes `phase.value = STAGE_PHASE_MAP[newStage] ?? phase.value`. New stages or new English forms are a one-line addition.
|
||||||
|
- **Rationale**: smallest possible refactor that satisfies R4.1, R4.2, R4.3, and R4.4.
|
||||||
|
|
||||||
|
### Decision: localize the `Step5Interaction.vue` chat-history templating
|
||||||
|
|
||||||
|
- **Context**: Lines 725 and 727 construct a prompt string that is sent **to the LLM**, not displayed to the user. There is a tension between (a) keeping it Chinese (matches the existing Chinese-tuned report agent) and (b) localizing it via `t()` (correct for English users, consistent with how the rest of the file handles strings).
|
||||||
|
- **Alternatives Considered**:
|
||||||
|
1. Leave the literals — fails R2.3 of the spec; freezes the user's language back into Chinese once they enter the chat history flow.
|
||||||
|
2. Always send English — breaks current Chinese behaviour.
|
||||||
|
3. Localize via `t()` so the prompt language follows the active locale — selected.
|
||||||
|
- **Selected Approach**: introduce `step5.chatRolePrompter`, `step5.chatRoleYou`, `step5.chatHistoryPrefix`, `step5.chatNewQuestionPrefix`. Build the prompt with these keys.
|
||||||
|
- **Rationale**: report agents in this project run on multilingual LLMs (Qwen, GLM, MiniMax) that handle either input language. The Chinese phrasing is preserved exactly in `zh.json` so Chinese users see no behaviour change.
|
||||||
|
- **Trade-offs**: a Chinese-locale user chatting against an English-tuned model would have a slight mismatch — but this combination is not the production path, and would be a separate issue if/when it arises.
|
||||||
|
- **Follow-up**: if the report agent is later forced to a single language, revisit and pin to that language.
|
||||||
|
|
||||||
|
## Risks & Mitigations
|
||||||
|
|
||||||
|
- **R5 — encoding speculative English markers** that don't match what the backend ultimately emits → Mitigation: do not encode English alternates for markers whose translated wording is undecided; centralise into `REPORT_MARKERS` so the future backend spec can update them in one place. Add an explicit allowlist of deliberate Chinese tokens to the audit (Requirement 6).
|
||||||
|
- **R3 — locale-file drift** (en.json/zh.json key sets diverging) → Mitigation: add the `keys_unsorted_recursive` parity diff to the verification script in R6.
|
||||||
|
- **R1 — Process.vue review fatigue** (~40 substitutions in a 50KB file) → Mitigation: split the implementation tasks by logical block (header / building progress / errors / fallbacks / project-info modal) so the PR is reviewable section by section.
|
||||||
|
- **R2.3 — Step5 prompt language change** affects backend behaviour → Mitigation: preserve exact Chinese in `zh.json` so the production Chinese path is byte-identical to today; add an inline test or manual smoke-test note in the implementation tasks.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- GitHub issue #23 — current ticket
|
||||||
|
- GitHub issue #11 — i18n epic (parent)
|
||||||
|
- GitHub issue #25 — backend LLM-prompt context label translation (downstream coordination point)
|
||||||
|
- GitHub issue #20 / spec `.kiro/specs/i18n-backfill-zh-json/` — locale parity precedent
|
||||||
|
- Open spec `.kiro/specs/i18n-report-agent-prompts/` — sibling backend translation spec; pinpointed as the future edit-trigger for `REPORT_MARKERS`
|
||||||
|
- `backend/app/services/zep_tools.py:47-1720` — canonical source of every backend marker the frontend parses
|
||||||
|
- vue-i18n 11 docs (already adopted; no new library decisions)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"feature_name": "i18n-frontend-ui-strings",
|
||||||
|
"created_at": "2026-05-07T21:24:46Z",
|
||||||
|
"updated_at": "2026-05-07T21:45:00Z",
|
||||||
|
"language": "en",
|
||||||
|
"phase": "tasks-generated",
|
||||||
|
"approvals": {
|
||||||
|
"requirements": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": true
|
||||||
|
},
|
||||||
|
"design": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": true
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"generated": true,
|
||||||
|
"approved": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ready_for_implementation": true,
|
||||||
|
"ticket": "23",
|
||||||
|
"ticket_url": "https://github.com/salestech-group/MiroFish/issues/23"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Implementation Tasks — `i18n-frontend-ui-strings`
|
||||||
|
|
||||||
|
> Approved requirements and design. Tasks ordered Foundation → Core → Integration → Validation per the project's tasks-generation rule.
|
||||||
|
|
||||||
|
- [x] 1. Foundation: locale-file additions and audit tooling
|
||||||
|
|
||||||
|
- [x] 1.1 Add the new `process.*`, `step3.*`, `step4.*`, `step5.*` keys to `locales/en.json` (English-only values)
|
||||||
|
- Add a new top-level `process` namespace covering every literal flagged for `Process.vue` (header, status badges, progress hints, error messages, fallback names, project-info modal labels, environment-setup-coming-soon alert).
|
||||||
|
- Add `step3.startFailed` if not already present (verify against current file).
|
||||||
|
- Add `step4.selectionReason` and `step4.awaitingStart`.
|
||||||
|
- Add `step5.chatRolePrompter`, `step5.chatRoleYou`, `step5.chatHistoryPrefix` (`{history}`, `{message}` ICU params).
|
||||||
|
- Strings carry idiomatic English wording only — **no Chinese characters in `en.json`**; the design's bilingual sketch is only a reviewer aid.
|
||||||
|
- Observable completion: `rg '[一-鿿]' locales/en.json` returns no hits in any newly added key (existing meta entries excluded).
|
||||||
|
- _Requirements: 1.5, 3.1, 3.3, 3.4_
|
||||||
|
|
||||||
|
- [x] 1.2 (P) Mirror the new keys to `locales/zh.json` with the original Chinese wording
|
||||||
|
- For each key added in 1.1, add an entry in `zh.json` carrying the **exact** Chinese string removed from the source files (no paraphrasing).
|
||||||
|
- Preserve existing namespace order; add new entries at the end of each namespace block to minimise diff noise.
|
||||||
|
- Observable completion: `jq -S 'paths(scalars) | join(".")' locales/en.json | sort -u` and the same for `zh.json` produce identical output.
|
||||||
|
- _Requirements: 3.1, 3.2, 3.4, 3.5_
|
||||||
|
- _Boundary: locales/zh.json_
|
||||||
|
- _Depends: 1.1_
|
||||||
|
|
||||||
|
- [x] 1.3 (P) Author the audit verifier `frontend/scripts/audit-i18n-strings.sh`
|
||||||
|
- Greps a CJK code-point range over the five files in scope only (no project-wide scan).
|
||||||
|
- Filters out an explicit allowlist: the `REPORT_MARKERS` literal block in `Step4Report.vue`, the bilingual log-severity helper, and any line with a trailing `// i18n-allow:<reason>` comment.
|
||||||
|
- Adds a key-parity check: `jq` over both locale files; reports keys missing from either side.
|
||||||
|
- Exits 0 on success, 1 with a human-readable list otherwise.
|
||||||
|
- Observable completion: running the script against the current branch (before the source-file changes) prints the expected hit list (sanity check); after the source-file changes it exits 0.
|
||||||
|
- _Requirements: 6.1, 6.2, 6.3, 6.4_
|
||||||
|
- _Boundary: frontend/scripts/_
|
||||||
|
|
||||||
|
- [x] 2. Core: externalize `Process.vue`
|
||||||
|
|
||||||
|
- [x] 2.1 Wire `vue-i18n` into `Process.vue`
|
||||||
|
- Add `import { useI18n } from 'vue-i18n'` and `const { t } = useI18n()` to the `<script setup>` block.
|
||||||
|
- Run a smoke check (`npm run dev`, open the page) to confirm the import does not regress the existing render.
|
||||||
|
- Observable completion: file compiles, dev server reloads, the page still renders identically (no Chinese strings replaced yet).
|
||||||
|
- _Requirements: 1.1, 2.6_
|
||||||
|
- _Boundary: Process.vue_
|
||||||
|
|
||||||
|
- [x] 2.2 Replace the graph-panel and header literals in `Process.vue`
|
||||||
|
- Substitute lines 26, 30, 32, 36, 39, 53 (header title, node/edge counts, refresh-button title, fullscreen toggle title, real-time-updating hint).
|
||||||
|
- Reuse `graph.*` keys where they already exist; introduce `process.graphPanelTitle`, `process.nodes`, `process.edges`, `process.refreshGraph`, `process.exitFullscreen`, `process.enterFullscreen`, `process.realtimeUpdating` only as needed.
|
||||||
|
- Observable completion: switch to `en` locale, reload, confirm the graph panel header reads in English; switch to `zh`, confirm the original Chinese is unchanged.
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3_
|
||||||
|
- _Boundary: Process.vue_
|
||||||
|
|
||||||
|
- [x] 2.3 Replace the build-flow section literals in `Process.vue`
|
||||||
|
- Substitute the ontology section (lines 174, 192, 193, 203, 204, 228, 237, 247, 249, 255, 264, 277, 298) and the graph-build section (lines 308, 318, 320, 326, 346, 350, 354, 366, 367, 378).
|
||||||
|
- Substitute the project-info modal labels (lines 388, 392, 396, 400, 404).
|
||||||
|
- Substitute the environment-setup-coming-soon alert at line 482.
|
||||||
|
- Observable completion: full walk of the build-flow on `en` locale shows English throughout; on `zh` matches today's wording.
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3_
|
||||||
|
- _Boundary: Process.vue_
|
||||||
|
|
||||||
|
- [x] 2.4 Replace the script-side error/status literals and fallback names in `Process.vue`
|
||||||
|
- Substitute the build-status computed (lines 452–458), the step-status computed (lines 536, 541, 543), the no-files error (line 563), and every error-assignment fallback through the watcher (lines 571, 598, 602, 634, 638, 657, 667, 673, 681, 686, 763, 778, 797).
|
||||||
|
- Substitute the D3 fallback labels (lines 872, 884, 900, 901): `t('process.waitingGraphData')`, `t('process.fallbackNodeName')`, `t('common.unknown')`.
|
||||||
|
- Observable completion: trigger an error path (e.g., upload no files) on `en` locale; the resulting error message renders in English.
|
||||||
|
- _Requirements: 1.1, 1.2, 1.3, 1.4_
|
||||||
|
- _Boundary: Process.vue_
|
||||||
|
|
||||||
|
- [x] 3. Core: externalize step components
|
||||||
|
|
||||||
|
- [x] 3.1 (P) Replace the `'启动失败'` fallback in `Step3Simulation.vue`
|
||||||
|
- Substitute `startError.value = res.error || '启动失败'` (line 423) with `t('step3.startFailed')`.
|
||||||
|
- Confirm `step3.startFailed` is present in both locale files (added in 1.1/1.2 if missing).
|
||||||
|
- Observable completion: trigger a backend simulation-start failure on `en` locale; the inline error message reads in English.
|
||||||
|
- _Requirements: 2.1, 2.4, 2.6_
|
||||||
|
- _Boundary: Step3Simulation.vue_
|
||||||
|
- _Depends: 1.1, 1.2_
|
||||||
|
|
||||||
|
- [x] 3.2 (P) Replace user-visible Chinese literals and centralize regex markers in `Step4Report.vue`
|
||||||
|
- Add a frozen `REPORT_MARKERS` constants block at the top of `<script setup>`, with one entry per backend-coupled marker (28 regex entries + a `noReply.is(value)` predicate + a `logSeverity.{isError,isWarning}(line)` helper). Each entry carries an inline comment naming the canonical backend source line in `zep_tools.py` (or other emitter).
|
||||||
|
- Refactor every parser call site to reference the block: `text.match(REPORT_MARKERS.analysisQuery.regex)`, `if (REPORT_MARKERS.noReply.is(interview.redditAnswer)) …`, etc. Touch every flagged line: 555, 557, 561, 565, 566, 567, 573, 580, 590, 597, 598, 609, 644, 652, 663, 673, 702, 706, 714, 816, 844, 845, 871, 893, 915, 923, 930, 943, 850, 854, 1325, 2005, 2006.
|
||||||
|
- Substitute the user-visible literals: line 1464 (`h('div', …, '选择理由')` → `t('step4.selectionReason')`), line 1774 (`'等待开始'` → `t('step4.awaitingStart')`).
|
||||||
|
- Mark the `REPORT_MARKERS` block with a leading `// i18n-allow: backend-coupled markers; sync with i18n-report-agent-prompts spec` comment so the audit script accepts the literals inside.
|
||||||
|
- Observable completion: open a finished project's report on `en` locale; key facts, core entities, relation chains, sub-queries, both interview platforms, and search results render with parity to `main`. The "selection reason" header and the "awaiting start" placeholder render in English.
|
||||||
|
- _Requirements: 2.2, 2.4, 2.6, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 5.10, 5.11_
|
||||||
|
- _Boundary: Step4Report.vue_
|
||||||
|
- _Depends: 1.1, 1.2_
|
||||||
|
|
||||||
|
- [x] 3.3 (P) Localize the chat-history templating in `Step5Interaction.vue`
|
||||||
|
- Substitute lines 721 and 723. The `historyContext` map becomes `t('step5.chatRolePrompter')` / `t('step5.chatRoleYou')`. The prompt template uses `t('step5.chatHistoryPrefix', { history: historyContext, message })`.
|
||||||
|
- Confirm the `zh.json` entries are byte-identical to the original Chinese phrasing so the production Chinese path is unchanged.
|
||||||
|
- Observable completion: on `en` locale, send a question and a follow-up; the second LLM response references the chat history coherently. On `zh` locale, the LLM behaviour is unchanged from `main` (smoke test).
|
||||||
|
- _Requirements: 2.3, 2.4, 2.6_
|
||||||
|
- _Boundary: Step5Interaction.vue_
|
||||||
|
- _Depends: 1.1, 1.2_
|
||||||
|
|
||||||
|
- [x] 3.4 (P) Refactor the stage watcher in `Step2EnvSetup.vue` to use `STAGE_PHASE_MAP`
|
||||||
|
- Add a `const STAGE_PHASE_MAP = Object.freeze({ '生成Agent人设': 1, 'generating_profiles': 1, '生成模拟配置': 2, 'generating_config': 2, '准备模拟脚本': 2, 'copying_scripts': 2 })` near other module-level constants.
|
||||||
|
- Rewrite the `watch(currentStage, …)` body so `phase.value = STAGE_PHASE_MAP[newStage] ?? phase.value`. Preserve the existing side-effect: when transitioning *into* phase 2, call `addLog(t('log.startGeneratingConfig'))` and start config polling.
|
||||||
|
- Mark the map with `// i18n-allow: backend stage tokens; multi-language tolerance` so the audit accepts the embedded Chinese.
|
||||||
|
- Observable completion: simulating each backend stage emission (e.g. via dev-tools console setting `currentStage.value = '生成Agent人设'`) drives `phase.value` to the expected value; same for the snake_case variant. A new English emission (e.g. `'generating profiles'`) added as a one-line map row works without other edits.
|
||||||
|
- _Requirements: 2.5, 2.6, 4.1, 4.2, 4.3, 4.4_
|
||||||
|
- _Boundary: Step2EnvSetup.vue_
|
||||||
|
|
||||||
|
- [x] 4. Integration and validation
|
||||||
|
|
||||||
|
- [x] 4.1 Run the audit verifier and resolve any remaining hits
|
||||||
|
- Execute `bash frontend/scripts/audit-i18n-strings.sh`.
|
||||||
|
- For each hit, either (a) substitute the literal with a `t()` call (and add the new key to both locale files), or (b) annotate the line with `// i18n-allow:<reason>` if the literal is deliberate.
|
||||||
|
- Re-run until the script exits 0.
|
||||||
|
- Observable completion: the script exits 0 with no stdout. The `git diff` against `main` shows no further user-visible Chinese in the five files outside the allowlisted blocks.
|
||||||
|
- _Requirements: 1.2, 2.4, 6.1, 6.2, 6.3, 6.4_
|
||||||
|
- _Depends: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4_
|
||||||
|
|
||||||
|
- [ ] 4.2 Manual end-to-end smoke test on both locales (deferred — requires running backend + browser; flagged in PR description for human reviewer)
|
||||||
|
- On `en` locale: start a fresh project (file upload → ontology → graph build → env setup → simulation → report → interaction). Verify no unexpected Chinese in the rendered DOM (excluding backend-emitted content currently in Chinese, which is out of scope per the spec boundary).
|
||||||
|
- On `zh` locale: same flow. Verify visual parity with `main` for every screen.
|
||||||
|
- On `en` locale: walk the chat flow in Step5 with one question and a follow-up; confirm the LLM response uses the prior turn coherently (validates the Step5 chat-history change).
|
||||||
|
- On `en` locale: open a previously generated report; confirm key facts, core entities, relation chains, sub-queries, both Twitter and Reddit interview answers, and search-result panes render with parity to `main`.
|
||||||
|
- Observable completion: a short note in the PR body listing the two locales tested, the routes walked, and any anomalies found (expected: none).
|
||||||
|
- _Requirements: 1.2, 1.3, 2.4, 5.10_
|
||||||
|
- _Depends: 4.1_
|
||||||
|
|
||||||
|
- [x] 4.3 Locale-parity sanity check
|
||||||
|
- Run `wc -l locales/en.json locales/zh.json`; line counts equal.
|
||||||
|
- Run the parity diff embedded in `audit-i18n-strings.sh`; no missing keys reported.
|
||||||
|
- Observable completion: both checks pass; the PR description quotes the verifier output (zero hits + parity OK).
|
||||||
|
- _Requirements: 3.1, 3.5, 6.1_
|
||||||
|
- _Depends: 4.1_
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit-i18n-strings.sh — verifier for issue #23 / spec i18n-frontend-ui-strings.
|
||||||
|
#
|
||||||
|
# Greps the five files in scope for hard-coded user-visible CJK literals and
|
||||||
|
# checks that locales/en.json and locales/zh.json have parity at every path.
|
||||||
|
#
|
||||||
|
# Annotation rules respected by this script:
|
||||||
|
# - lines that are pure // line comments are skipped
|
||||||
|
# - lines that contain `// i18n-allow:<reason>` are skipped (deliberate token)
|
||||||
|
# - lines that contain `console.log/info/warn/error/debug(` are skipped
|
||||||
|
# (developer logs, not user-visible UI; out of scope)
|
||||||
|
# - lines between `// i18n-allow-block:<reason>` and `// i18n-allow-block-end`
|
||||||
|
# are skipped (used for the REPORT_MARKERS and STAGE_PHASE_MAP blocks
|
||||||
|
# that intentionally embed Chinese tokens for backend compatibility)
|
||||||
|
#
|
||||||
|
# Exits 0 on success; non-zero with a human-readable list of issues otherwise.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
|
||||||
|
FILES=(
|
||||||
|
"frontend/src/views/Process.vue"
|
||||||
|
"frontend/src/components/Step2EnvSetup.vue"
|
||||||
|
"frontend/src/components/Step3Simulation.vue"
|
||||||
|
"frontend/src/components/Step4Report.vue"
|
||||||
|
"frontend/src/components/Step5Interaction.vue"
|
||||||
|
)
|
||||||
|
|
||||||
|
CJK_RE='[\x{4e00}-\x{9fff}\x{3000}-\x{303f}\x{ff00}-\x{ffef}]'
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
echo "audit: missing file $f" >&2
|
||||||
|
fail=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# awk filters out comments and dev-only constructs, then strips inline
|
||||||
|
# trailing comments before passing the surviving line to ripgrep. The
|
||||||
|
# remaining hits are user-visible CJK literals.
|
||||||
|
hits="$(awk '
|
||||||
|
BEGIN { in_allow = 0; in_html = 0; in_css = 0 }
|
||||||
|
|
||||||
|
# Spec-controlled allowlist regions (REPORT_MARKERS, STAGE_PHASE_MAP).
|
||||||
|
/\/\/ i18n-allow-block:/ { in_allow = 1; next }
|
||||||
|
/\/\/ i18n-allow-block-end/ { in_allow = 0; next }
|
||||||
|
in_allow { next }
|
||||||
|
|
||||||
|
# Per-line allow annotation.
|
||||||
|
/\/\/ i18n-allow:/ { next }
|
||||||
|
|
||||||
|
# Vue template HTML comments (<!-- ... -->), single- or multi-line.
|
||||||
|
{
|
||||||
|
line = $0
|
||||||
|
# Strip pairs of <!-- ... --> on the same line.
|
||||||
|
while (match(line, /<!--.*-->/)) {
|
||||||
|
line = substr(line, 1, RSTART - 1) substr(line, RSTART + RLENGTH)
|
||||||
|
}
|
||||||
|
# Detect entering/leaving an HTML comment block.
|
||||||
|
if (in_html) {
|
||||||
|
if (match(line, /-->/)) {
|
||||||
|
line = substr(line, RSTART + RLENGTH)
|
||||||
|
in_html = 0
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match(line, /<!--/)) {
|
||||||
|
line = substr(line, 1, RSTART - 1)
|
||||||
|
in_html = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# CSS / block JS comments (/* ... */).
|
||||||
|
{
|
||||||
|
while (match(line, /\/\*.*\*\//)) {
|
||||||
|
line = substr(line, 1, RSTART - 1) substr(line, RSTART + RLENGTH)
|
||||||
|
}
|
||||||
|
if (in_css) {
|
||||||
|
if (match(line, /\*\//)) {
|
||||||
|
line = substr(line, RSTART + RLENGTH)
|
||||||
|
in_css = 0
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match(line, /\/\*/)) {
|
||||||
|
line = substr(line, 1, RSTART - 1)
|
||||||
|
in_css = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strip inline trailing JS line comments (// ...).
|
||||||
|
# Naive: if " //" appears, drop everything from there onwards.
|
||||||
|
# Vue template attributes do not legitimately contain "//" outside URLs;
|
||||||
|
# URLs in string literals stay intact because the regex requires a
|
||||||
|
# leading whitespace before the //.
|
||||||
|
{
|
||||||
|
sub(/[[:space:]]\/\/.*$/, "", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Whole-line single-line comments and block-comment continuations.
|
||||||
|
line ~ /^[[:space:]]*\/\// { next }
|
||||||
|
line ~ /^[[:space:]]*\*/ { next }
|
||||||
|
|
||||||
|
# Developer-only console emissions: not user-visible.
|
||||||
|
line ~ /console\.(log|info|warn|error|debug)\(/ { next }
|
||||||
|
|
||||||
|
line { print NR ":" line }
|
||||||
|
' "$f" | grep -P "$CJK_RE" || true)"
|
||||||
|
if [[ -n "$hits" ]]; then
|
||||||
|
echo "$f:" >&2
|
||||||
|
echo "$hits" | sed "s|^| $f:|" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Locale parity check: every path that resolves to a scalar in en.json must also
|
||||||
|
# exist in zh.json, and vice versa.
|
||||||
|
parity_diff="$(python3 - <<'PY'
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
def paths(d, prefix=""):
|
||||||
|
out = []
|
||||||
|
if isinstance(d, dict):
|
||||||
|
for k, v in d.items():
|
||||||
|
out.extend(paths(v, f"{prefix}.{k}" if prefix else k))
|
||||||
|
elif isinstance(d, list):
|
||||||
|
for i, v in enumerate(d):
|
||||||
|
out.extend(paths(v, f"{prefix}[{i}]"))
|
||||||
|
else:
|
||||||
|
out.append(prefix)
|
||||||
|
return out
|
||||||
|
|
||||||
|
with open("locales/en.json", encoding="utf-8") as f:
|
||||||
|
en = json.load(f)
|
||||||
|
with open("locales/zh.json", encoding="utf-8") as f:
|
||||||
|
zh = json.load(f)
|
||||||
|
|
||||||
|
en_paths = set(paths(en))
|
||||||
|
zh_paths = set(paths(zh))
|
||||||
|
|
||||||
|
only_en = sorted(en_paths - zh_paths)
|
||||||
|
only_zh = sorted(zh_paths - en_paths)
|
||||||
|
|
||||||
|
if only_en:
|
||||||
|
print("missing in zh.json:")
|
||||||
|
for p in only_en:
|
||||||
|
print(f" {p}")
|
||||||
|
if only_zh:
|
||||||
|
print("missing in en.json:")
|
||||||
|
for p in only_zh:
|
||||||
|
print(f" {p}")
|
||||||
|
|
||||||
|
sys.exit(1 if (only_en or only_zh) else 0)
|
||||||
|
PY
|
||||||
|
)" || parity_status=$?
|
||||||
|
|
||||||
|
if [[ -n "${parity_diff:-}" ]]; then
|
||||||
|
echo "$parity_diff" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sentinel: en.json must contain no CJK characters in its scalar values
|
||||||
|
# (issue #20 / spec i18n-backfill-zh-json regression guard).
|
||||||
|
en_cjk="$(grep -nP "$CJK_RE" locales/en.json || true)"
|
||||||
|
if [[ -n "$en_cjk" ]]; then
|
||||||
|
echo "locales/en.json contains CJK characters (regression vs #20):" >&2
|
||||||
|
echo "$en_cjk" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$fail"
|
||||||
|
|
@ -675,19 +675,33 @@ let lastLoggedConfigStage = ''
|
||||||
const useCustomRounds = ref(false) // Default: use the auto-derived round count.
|
const useCustomRounds = ref(false) // Default: use the auto-derived round count.
|
||||||
const customMaxRounds = ref(40) // Default recommendation: 40 rounds.
|
const customMaxRounds = ref(40) // Default recommendation: 40 rounds.
|
||||||
|
|
||||||
|
// Backend stage tokens. Each row maps a backend-emitted stage name to a phase
|
||||||
|
// number. Both legacy Chinese display strings and the snake_case identifiers
|
||||||
|
// are kept so the watcher remains correct before, during, and after the
|
||||||
|
// downstream backend prompt translation (issue #25 / spec
|
||||||
|
// i18n-report-agent-prompts). Adding a new stage form is a one-line edit.
|
||||||
|
// i18n-allow-block: backend stage tokens; multi-language tolerance per spec
|
||||||
|
// i18n-frontend-ui-strings, requirement 4.
|
||||||
|
const STAGE_PHASE_MAP = Object.freeze({
|
||||||
|
'生成Agent人设': 1,
|
||||||
|
'generating_profiles': 1,
|
||||||
|
'生成模拟配置': 2,
|
||||||
|
'generating_config': 2,
|
||||||
|
'准备模拟脚本': 2,
|
||||||
|
'copying_scripts': 2,
|
||||||
|
})
|
||||||
|
// i18n-allow-block-end
|
||||||
|
|
||||||
// Watch stage to update phase
|
// Watch stage to update phase
|
||||||
watch(currentStage, (newStage) => {
|
watch(currentStage, (newStage, oldStage) => {
|
||||||
if (newStage === '生成Agent人设' || newStage === 'generating_profiles') {
|
const newPhase = STAGE_PHASE_MAP[newStage]
|
||||||
phase.value = 1
|
if (newPhase === undefined) return
|
||||||
} else if (newStage === '生成模拟配置' || newStage === 'generating_config') {
|
phase.value = newPhase
|
||||||
phase.value = 2
|
// Trigger config polling on the transition into phase 2.
|
||||||
// Entering the config-generation stage — start polling the config endpoint.
|
const wasPhase2 = STAGE_PHASE_MAP[oldStage] === 2
|
||||||
if (!configTimer) {
|
if (newPhase === 2 && !wasPhase2 && !configTimer) {
|
||||||
addLog(t('log.startGeneratingConfig'))
|
addLog(t('log.startGeneratingConfig'))
|
||||||
startConfigPolling()
|
startConfigPolling()
|
||||||
}
|
|
||||||
} else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') {
|
|
||||||
phase.value = 2 // Still part of the config stage.
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,7 @@ const doStartSimulation = async () => {
|
||||||
startStatusPolling()
|
startStatusPolling()
|
||||||
startDetailPolling()
|
startDetailPolling()
|
||||||
} else {
|
} else {
|
||||||
startError.value = res.error || '启动失败'
|
startError.value = res.error || t('step3.startFailedFallback')
|
||||||
addLog(t('log.startFailed', { error: res.error || t('common.unknownError') }))
|
addLog(t('log.startFailed', { error: res.error || t('common.unknownError') }))
|
||||||
emit('update-status', 'error')
|
emit('update-status', 'error')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,110 @@ const getToolIcon = (toolName) => {
|
||||||
return toolConfig[toolName]?.icon || 'tool'
|
return toolConfig[toolName]?.icon || 'tool'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backend-coupled markers used to parse the report-agent output. Centralised
|
||||||
|
// here so that, when the backend prompt translation lands (issue #25 / spec
|
||||||
|
// i18n-report-agent-prompts), updating the alternates is a single-file edit.
|
||||||
|
// Until then these stay Chinese-only, matching what backend/app/services/
|
||||||
|
// zep_tools.py emits today (line numbers in comments are the canonical
|
||||||
|
// source). Per Requirement 5, parsers must keep working as the backend
|
||||||
|
// transitions; a translated marker is added by appending an alternation
|
||||||
|
// branch to the relevant regex.
|
||||||
|
// i18n-allow-block: backend-coupled markers; sync with i18n-report-agent-prompts
|
||||||
|
const REPORT_MARKERS = Object.freeze({
|
||||||
|
// zep_tools.py:175 — f"分析问题: {self.query}"
|
||||||
|
analysisQuery: { regex: /分析问题:\s*(.+?)(?:\n|$)/ },
|
||||||
|
// zep_tools.py:176 — f"预测场景: {self.simulation_requirement}"
|
||||||
|
predictionScene: { regex: /预测场景:\s*(.+?)(?:\n|$)/ },
|
||||||
|
// zep_tools.py:178 — f"- 相关预测事实: {self.total_facts}条"
|
||||||
|
factsCount: { regex: /相关预测事实:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:179 — f"- 涉及实体: {self.total_entities}个"
|
||||||
|
entitiesCount: { regex: /涉及实体:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:180 — f"- 关系链: {self.total_relationships}条"
|
||||||
|
relationsCount: { regex: /关系链:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:185 — section header
|
||||||
|
subQueriesHeader: { regex: /### 分析的子问题\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:191 — section header
|
||||||
|
keyFactsHeader: { regex: /### 【关键事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:197 — section header
|
||||||
|
coreEntitiesHdr: { regex: /### 【核心实体】\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:201 — f" 摘要: \"...\""
|
||||||
|
entitySummary: { regex: /摘要:\s*"?(.+?)"?(?:\n|$)/ },
|
||||||
|
// zep_tools.py:203 — f" 相关事实: ..."
|
||||||
|
relatedFactsCnt: { regex: /相关事实:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:207 — section header
|
||||||
|
relationChainHdr: { regex: /### 【关系链】\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// PanoramaSearch — query line
|
||||||
|
panoramaQuery: { regex: /查询:\s*(.+?)(?:\n|$)/ },
|
||||||
|
// PanoramaSearch — node and edge totals
|
||||||
|
totalNodes: { regex: /总节点数:\s*(\d+)/ },
|
||||||
|
totalEdges: { regex: /总边数:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:258 — f"- 当前有效事实: {self.active_count}条"
|
||||||
|
activeFactsCnt: { regex: /当前有效事实:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:259 — f"- 历史/过期事实: {self.historical_count}条"
|
||||||
|
historicalCnt: { regex: /历史\/过期事实:\s*(\d+)/ },
|
||||||
|
// zep_tools.py:264 — section header
|
||||||
|
activeFactsHdr: { regex: /### 【当前有效事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:270 — section header
|
||||||
|
historicalHdr: { regex: /### 【历史\/过期事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:276 — section header
|
||||||
|
involvedEntities: { regex: /### 【涉及实体】\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// zep_tools.py:379 — f"**采访主题:** {self.interview_topic}"
|
||||||
|
interviewTopic: { regex: /\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/ },
|
||||||
|
// zep_tools.py:380 — f"**采访人数:** {n} / {m} 位模拟Agent"
|
||||||
|
interviewCount: { regex: /\*\*采访人数:\*\*\s*(\d+)\s*\/\s*(\d+)/ },
|
||||||
|
// zep_tools.py:381 — section header
|
||||||
|
selectionReasonHdr: { regex: /### 采访对象选择理由\n([\s\S]*?)(?=\n---\n|\n### 采访实录)/ },
|
||||||
|
// selection-reason line formats (3 variants)
|
||||||
|
selectionFormat1: { regex: /^\d+\.\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/ },
|
||||||
|
selectionFormat2: { regex: /^-\s*选择([^((]+)(?:[((]index\s*=?\s*\d+[))])?[::]\s*(.*)/ },
|
||||||
|
selectionFormat3: { regex: /^-\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/ },
|
||||||
|
// selection-reason terminator phrases (lines that end the per-person reason)
|
||||||
|
selectionTerminator: { regex: /^未选|^综上|^最终选择/ },
|
||||||
|
// interview block delimiter (from zep_tools.py)
|
||||||
|
interviewBlockSplit: { regex: /#### 采访 #\d+:/ },
|
||||||
|
// bio line — `_简介: ..._\n`
|
||||||
|
agentBio: { regex: /_简介:\s*([\s\S]*?)_\n/ },
|
||||||
|
// platform answer markers — zep_tools.py:1426
|
||||||
|
twitterAnswer: { regex: /【Twitter平台回答】\n?([\s\S]*?)(?=【Reddit平台回答】|$)/ },
|
||||||
|
redditAnswer: { regex: /【Reddit平台回答】\n?([\s\S]*?)$/ },
|
||||||
|
// zep_tools.py:311 — section header (key quotes)
|
||||||
|
keyQuotesHeader: { regex: /\*\*A:\*\*\s*([\s\S]*?)(?=\*\*关键引言|$)/ },
|
||||||
|
keyQuotesBlock: { regex: /\*\*关键引言:\*\*\n([\s\S]*?)(?=\n---|\n####|$)/ },
|
||||||
|
// zep_tools.py:395 — section header
|
||||||
|
interviewSummary: { regex: /### 采访摘要与核心观点\n([\s\S]*?)$/ },
|
||||||
|
// QuickSearch — search query line and result count
|
||||||
|
searchQuery: { regex: /搜索查询:\s*(.+?)(?:\n|$)/ },
|
||||||
|
searchCount: { regex: /找到\s*(\d+)\s*条/ },
|
||||||
|
// QuickSearch section headers
|
||||||
|
relatedFactsHdr: { regex: /### 相关事实:\n([\s\S]*)$/ },
|
||||||
|
relatedEdgesHdr: { regex: /### 相关边:\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
relatedNodesHdr: { regex: /### 相关节点:\n([\s\S]*?)(?=\n###|$)/ },
|
||||||
|
// numbered question prefix used inside an answer block
|
||||||
|
numberedQuestion: { regex: /(?:^|[\r\n]+)问题(\d+)[::]\s*/g },
|
||||||
|
numberedQuestionStrip: { regex: /^问题\d+[::]\s*/ },
|
||||||
|
// ordered-list prefix variants used by the report-agent
|
||||||
|
numberedListPrefix: { regex: /^\s*\d+[\.\、\))]\s*/ },
|
||||||
|
// Final-answer marker emitted by the ReACT loop in the report agent.
|
||||||
|
// Matches the legacy Chinese form; English ("Final Answer:") is already
|
||||||
|
// handled by a separate regex earlier in extractFinalContent.
|
||||||
|
finalAnswerCn: { regex: /最终答案[::]\s*\n*([\s\S]*)$/i },
|
||||||
|
// No-reply marker — exact-match predicate.
|
||||||
|
noReply: {
|
||||||
|
is(value) {
|
||||||
|
return value === '(该平台未获得回复)'
|
||||||
|
|| value === '(该平台未获得回复)'
|
||||||
|
|| value === '[无回复]'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Log severity classification — backend logs interleave English logging
|
||||||
|
// levels with legacy Chinese phrasing.
|
||||||
|
logSeverity: {
|
||||||
|
isError(line) { return line.includes('ERROR') || line.includes('错误') },
|
||||||
|
isWarning(line) { return line.includes('WARNING') || line.includes('警告') },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// i18n-allow-block-end
|
||||||
|
|
||||||
// Parse functions
|
// Parse functions
|
||||||
const parseInsightForge = (text) => {
|
const parseInsightForge = (text) => {
|
||||||
const result = {
|
const result = {
|
||||||
|
|
@ -551,31 +655,31 @@ const parseInsightForge = (text) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the analysis question.
|
// 提取分析问题
|
||||||
const queryMatch = text.match(/分析问题:\s*(.+?)(?:\n|$)/)
|
const queryMatch = text.match(REPORT_MARKERS.analysisQuery.regex)
|
||||||
if (queryMatch) result.query = queryMatch[1].trim()
|
if (queryMatch) result.query = queryMatch[1].trim()
|
||||||
|
|
||||||
// Extract the prediction scenario.
|
// 提取预测场景
|
||||||
const reqMatch = text.match(/预测场景:\s*(.+?)(?:\n|$)/)
|
const reqMatch = text.match(REPORT_MARKERS.predictionScene.regex)
|
||||||
if (reqMatch) result.simulationRequirement = reqMatch[1].trim()
|
if (reqMatch) result.simulationRequirement = reqMatch[1].trim()
|
||||||
|
|
||||||
// Extract counters from the "相关预测事实: X条" format.
|
// 提取统计数据 - 匹配"相关预测事实: X条"格式
|
||||||
const factMatch = text.match(/相关预测事实:\s*(\d+)/)
|
const factMatch = text.match(REPORT_MARKERS.factsCount.regex)
|
||||||
const entityMatch = text.match(/涉及实体:\s*(\d+)/)
|
const entityMatch = text.match(REPORT_MARKERS.entitiesCount.regex)
|
||||||
const relMatch = text.match(/关系链:\s*(\d+)/)
|
const relMatch = text.match(REPORT_MARKERS.relationsCount.regex)
|
||||||
if (factMatch) result.stats.facts = parseInt(factMatch[1])
|
if (factMatch) result.stats.facts = parseInt(factMatch[1])
|
||||||
if (entityMatch) result.stats.entities = parseInt(entityMatch[1])
|
if (entityMatch) result.stats.entities = parseInt(entityMatch[1])
|
||||||
if (relMatch) result.stats.relationships = parseInt(relMatch[1])
|
if (relMatch) result.stats.relationships = parseInt(relMatch[1])
|
||||||
|
|
||||||
// Extract sub-questions in full (no cap).
|
// 提取子问题 - 完整提取,不限制数量
|
||||||
const subQSection = text.match(/### 分析的子问题\n([\s\S]*?)(?=\n###|$)/)
|
const subQSection = text.match(REPORT_MARKERS.subQueriesHeader.regex)
|
||||||
if (subQSection) {
|
if (subQSection) {
|
||||||
const lines = subQSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
const lines = subQSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||||||
result.subQueries = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
|
result.subQueries = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract key facts in full (no cap).
|
// 提取关键事实 - 完整提取,不限制数量
|
||||||
const factsSection = text.match(/### 【关键事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
|
const factsSection = text.match(REPORT_MARKERS.keyFactsHeader.regex)
|
||||||
if (factsSection) {
|
if (factsSection) {
|
||||||
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||||||
result.facts = lines.map(l => {
|
result.facts = lines.map(l => {
|
||||||
|
|
@ -584,16 +688,16 @@ const parseInsightForge = (text) => {
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract core entities — includes summary and related-fact count.
|
// 提取核心实体 - 完整提取,包含摘要和相关事实数
|
||||||
const entitySection = text.match(/### 【核心实体】\n([\s\S]*?)(?=\n###|$)/)
|
const entitySection = text.match(REPORT_MARKERS.coreEntitiesHdr.regex)
|
||||||
if (entitySection) {
|
if (entitySection) {
|
||||||
const entityText = entitySection[1]
|
const entityText = entitySection[1]
|
||||||
// Split entity blocks on the "- **" markdown bullet.
|
// Split entity blocks on the "- **" markdown bullet.
|
||||||
const entityBlocks = entityText.split(/\n(?=- \*\*)/).filter(b => b.trim().startsWith('- **'))
|
const entityBlocks = entityText.split(/\n(?=- \*\*)/).filter(b => b.trim().startsWith('- **'))
|
||||||
result.entities = entityBlocks.map(block => {
|
result.entities = entityBlocks.map(block => {
|
||||||
const nameMatch = block.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
|
const nameMatch = block.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
|
||||||
const summaryMatch = block.match(/摘要:\s*"?(.+?)"?(?:\n|$)/)
|
const summaryMatch = block.match(REPORT_MARKERS.entitySummary.regex)
|
||||||
const relatedMatch = block.match(/相关事实:\s*(\d+)/)
|
const relatedMatch = block.match(REPORT_MARKERS.relatedFactsCnt.regex)
|
||||||
return {
|
return {
|
||||||
name: nameMatch ? nameMatch[1].trim() : '',
|
name: nameMatch ? nameMatch[1].trim() : '',
|
||||||
type: nameMatch ? nameMatch[2].trim() : '',
|
type: nameMatch ? nameMatch[2].trim() : '',
|
||||||
|
|
@ -602,9 +706,9 @@ const parseInsightForge = (text) => {
|
||||||
}
|
}
|
||||||
}).filter(e => e.name)
|
}).filter(e => e.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract relationship chains in full (no cap).
|
// 提取关系链 - 完整提取,不限制数量
|
||||||
const relSection = text.match(/### 【关系链】\n([\s\S]*?)(?=\n###|$)/)
|
const relSection = text.match(REPORT_MARKERS.relationChainHdr.regex)
|
||||||
if (relSection) {
|
if (relSection) {
|
||||||
const lines = relSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
const lines = relSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||||||
result.relations = lines.map(l => {
|
result.relations = lines.map(l => {
|
||||||
|
|
@ -632,22 +736,22 @@ const parsePanorama = (text) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the query.
|
// 提取查询
|
||||||
const queryMatch = text.match(/查询:\s*(.+?)(?:\n|$)/)
|
const queryMatch = text.match(REPORT_MARKERS.panoramaQuery.regex)
|
||||||
if (queryMatch) result.query = queryMatch[1].trim()
|
if (queryMatch) result.query = queryMatch[1].trim()
|
||||||
|
|
||||||
// Extract counter stats.
|
// 提取统计数据
|
||||||
const nodesMatch = text.match(/总节点数:\s*(\d+)/)
|
const nodesMatch = text.match(REPORT_MARKERS.totalNodes.regex)
|
||||||
const edgesMatch = text.match(/总边数:\s*(\d+)/)
|
const edgesMatch = text.match(REPORT_MARKERS.totalEdges.regex)
|
||||||
const activeMatch = text.match(/当前有效事实:\s*(\d+)/)
|
const activeMatch = text.match(REPORT_MARKERS.activeFactsCnt.regex)
|
||||||
const histMatch = text.match(/历史\/过期事实:\s*(\d+)/)
|
const histMatch = text.match(REPORT_MARKERS.historicalCnt.regex)
|
||||||
if (nodesMatch) result.stats.nodes = parseInt(nodesMatch[1])
|
if (nodesMatch) result.stats.nodes = parseInt(nodesMatch[1])
|
||||||
if (edgesMatch) result.stats.edges = parseInt(edgesMatch[1])
|
if (edgesMatch) result.stats.edges = parseInt(edgesMatch[1])
|
||||||
if (activeMatch) result.stats.activeFacts = parseInt(activeMatch[1])
|
if (activeMatch) result.stats.activeFacts = parseInt(activeMatch[1])
|
||||||
if (histMatch) result.stats.historicalFacts = parseInt(histMatch[1])
|
if (histMatch) result.stats.historicalFacts = parseInt(histMatch[1])
|
||||||
|
|
||||||
// Extract currently valid facts in full (no cap).
|
// 提取当前有效事实 - 完整提取,不限制数量
|
||||||
const activeSection = text.match(/### 【当前有效事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
|
const activeSection = text.match(REPORT_MARKERS.activeFactsHdr.regex)
|
||||||
if (activeSection) {
|
if (activeSection) {
|
||||||
const lines = activeSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
const lines = activeSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||||||
result.activeFacts = lines.map(l => {
|
result.activeFacts = lines.map(l => {
|
||||||
|
|
@ -657,8 +761,8 @@ const parsePanorama = (text) => {
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract historical/expired facts in full (no cap).
|
// 提取历史/过期事实 - 完整提取,不限制数量
|
||||||
const histSection = text.match(/### 【历史\/过期事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
|
const histSection = text.match(REPORT_MARKERS.historicalHdr.regex)
|
||||||
if (histSection) {
|
if (histSection) {
|
||||||
const lines = histSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
const lines = histSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||||||
result.historicalFacts = lines.map(l => {
|
result.historicalFacts = lines.map(l => {
|
||||||
|
|
@ -666,9 +770,9 @@ const parsePanorama = (text) => {
|
||||||
return factText
|
return factText
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract referenced entities in full (no cap).
|
// 提取涉及实体 - 完整提取,不限制数量
|
||||||
const entitySection = text.match(/### 【涉及实体】\n([\s\S]*?)(?=\n###|$)/)
|
const entitySection = text.match(REPORT_MARKERS.involvedEntities.regex)
|
||||||
if (entitySection) {
|
if (entitySection) {
|
||||||
const lines = entitySection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
const lines = entitySection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||||||
result.entities = lines.map(l => {
|
result.entities = lines.map(l => {
|
||||||
|
|
@ -696,25 +800,25 @@ const parseInterview = (text) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the interview topic.
|
// 提取采访主题
|
||||||
const topicMatch = text.match(/\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/)
|
const topicMatch = text.match(REPORT_MARKERS.interviewTopic.regex)
|
||||||
if (topicMatch) result.topic = topicMatch[1].trim()
|
if (topicMatch) result.topic = topicMatch[1].trim()
|
||||||
|
|
||||||
// Extract the interview-count line, e.g. "5 / 9 位模拟Agent".
|
// 提取采访人数(如 "5 / 9 位模拟Agent")
|
||||||
const countMatch = text.match(/\*\*采访人数:\*\*\s*(\d+)\s*\/\s*(\d+)/)
|
const countMatch = text.match(REPORT_MARKERS.interviewCount.regex)
|
||||||
if (countMatch) {
|
if (countMatch) {
|
||||||
result.successCount = parseInt(countMatch[1])
|
result.successCount = parseInt(countMatch[1])
|
||||||
result.totalCount = parseInt(countMatch[2])
|
result.totalCount = parseInt(countMatch[2])
|
||||||
result.agentCount = `${countMatch[1]} / ${countMatch[2]}`
|
result.agentCount = `${countMatch[1]} / ${countMatch[2]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the rationale for the interviewee selection.
|
// 提取采访对象选择理由
|
||||||
const reasonMatch = text.match(/### 采访对象选择理由\n([\s\S]*?)(?=\n---\n|\n### 采访实录)/)
|
const reasonMatch = text.match(REPORT_MARKERS.selectionReasonHdr.regex)
|
||||||
if (reasonMatch) {
|
if (reasonMatch) {
|
||||||
result.selectionReason = reasonMatch[1].trim()
|
result.selectionReason = reasonMatch[1].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse each interviewee's individual rationale out of the rationale section.
|
// 解析每个人的选择理由
|
||||||
const parseIndividualReasons = (reasonText) => {
|
const parseIndividualReasons = (reasonText) => {
|
||||||
const reasons = {}
|
const reasons = {}
|
||||||
if (!reasonText) return reasons
|
if (!reasonText) return reasons
|
||||||
|
|
@ -728,25 +832,28 @@ const parseInterview = (text) => {
|
||||||
let name = null
|
let name = null
|
||||||
let reasonStart = null
|
let reasonStart = null
|
||||||
|
|
||||||
// Format 1: "<n>. **<name>(index=<i>)**:<reason>"
|
// 格式1: 数字. **名字(index=X)**:理由
|
||||||
headerMatch = line.match(/^\d+\.\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/)
|
// 例如: 1. **校友_345(index=1)**:作为武大校友...
|
||||||
|
headerMatch = line.match(REPORT_MARKERS.selectionFormat1.regex)
|
||||||
if (headerMatch) {
|
if (headerMatch) {
|
||||||
name = headerMatch[1].trim()
|
name = headerMatch[1].trim()
|
||||||
reasonStart = headerMatch[2]
|
reasonStart = headerMatch[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 2: "- 选择<name>(index <i>):<reason>"
|
// 格式2: - 选择名字(index X):理由
|
||||||
|
// 例如: - 选择家长_601(index 0):作为家长群体代表...
|
||||||
if (!headerMatch) {
|
if (!headerMatch) {
|
||||||
headerMatch = line.match(/^-\s*选择([^((]+)(?:[((]index\s*=?\s*\d+[))])?[::]\s*(.*)/)
|
headerMatch = line.match(REPORT_MARKERS.selectionFormat2.regex)
|
||||||
if (headerMatch) {
|
if (headerMatch) {
|
||||||
name = headerMatch[1].trim()
|
name = headerMatch[1].trim()
|
||||||
reasonStart = headerMatch[2]
|
reasonStart = headerMatch[2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format 3: "- **<name>(index <i>)**:<reason>"
|
// 格式3: - **名字(index X)**:理由
|
||||||
|
// 例如: - **家长_601(index 0)**:作为家长群体代表...
|
||||||
if (!headerMatch) {
|
if (!headerMatch) {
|
||||||
headerMatch = line.match(/^-\s*\*\*([^*((]+)(?:[((]index\s*=?\s*\d+[))])?\*\*[::]\s*(.*)/)
|
headerMatch = line.match(REPORT_MARKERS.selectionFormat3.regex)
|
||||||
if (headerMatch) {
|
if (headerMatch) {
|
||||||
name = headerMatch[1].trim()
|
name = headerMatch[1].trim()
|
||||||
reasonStart = headerMatch[2]
|
reasonStart = headerMatch[2]
|
||||||
|
|
@ -760,12 +867,13 @@ const parseInterview = (text) => {
|
||||||
}
|
}
|
||||||
currentName = name
|
currentName = name
|
||||||
currentReason = reasonStart ? [reasonStart.trim()] : []
|
currentReason = reasonStart ? [reasonStart.trim()] : []
|
||||||
} else if (currentName && line.trim() && !line.match(/^未选|^综上|^最终选择/)) {
|
} else if (currentName && line.trim() && !line.match(REPORT_MARKERS.selectionTerminator.regex)) {
|
||||||
// Continuation line for the current rationale (skip closing-summary paragraphs).
|
// 理由的续行(排除结尾总结段落)
|
||||||
currentReason.push(line.trim())
|
currentReason.push(line.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存最后一个人的理由
|
||||||
if (currentName && currentReason.length > 0) {
|
if (currentName && currentReason.length > 0) {
|
||||||
reasons[currentName] = currentReason.join(' ').trim()
|
reasons[currentName] = currentReason.join(' ').trim()
|
||||||
}
|
}
|
||||||
|
|
@ -775,8 +883,8 @@ const parseInterview = (text) => {
|
||||||
|
|
||||||
const individualReasons = parseIndividualReasons(result.selectionReason)
|
const individualReasons = parseIndividualReasons(result.selectionReason)
|
||||||
|
|
||||||
// Extract each interview record.
|
// 提取每个采访记录
|
||||||
const interviewBlocks = text.split(/#### 采访 #\d+:/).slice(1)
|
const interviewBlocks = text.split(REPORT_MARKERS.interviewBlockSplit.regex).slice(1)
|
||||||
|
|
||||||
interviewBlocks.forEach((block, index) => {
|
interviewBlocks.forEach((block, index) => {
|
||||||
const interview = {
|
const interview = {
|
||||||
|
|
@ -805,8 +913,8 @@ const parseInterview = (text) => {
|
||||||
interview.selectionReason = individualReasons[interview.name] || ''
|
interview.selectionReason = individualReasons[interview.name] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the bio.
|
// 提取简介
|
||||||
const bioMatch = block.match(/_简介:\s*([\s\S]*?)_\n/)
|
const bioMatch = block.match(REPORT_MARKERS.agentBio.regex)
|
||||||
if (bioMatch) {
|
if (bioMatch) {
|
||||||
interview.bio = bioMatch[1].trim().replace(/\.\.\.$/, '...')
|
interview.bio = bioMatch[1].trim().replace(/\.\.\.$/, '...')
|
||||||
}
|
}
|
||||||
|
|
@ -828,30 +936,30 @@ const parseInterview = (text) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract answers, split by Twitter and Reddit.
|
// 提取回答 - 分Twitter和Reddit
|
||||||
const answerMatch = block.match(/\*\*A:\*\*\s*([\s\S]*?)(?=\*\*关键引言|$)/)
|
const answerMatch = block.match(REPORT_MARKERS.keyQuotesHeader.regex)
|
||||||
if (answerMatch) {
|
if (answerMatch) {
|
||||||
const answerText = answerMatch[1].trim()
|
const answerText = answerMatch[1].trim()
|
||||||
|
|
||||||
// Split into separate Twitter and Reddit answers.
|
// 分离Twitter和Reddit回答
|
||||||
const twitterMatch = answerText.match(/【Twitter平台回答】\n?([\s\S]*?)(?=【Reddit平台回答】|$)/)
|
const twitterMatch = answerText.match(REPORT_MARKERS.twitterAnswer.regex)
|
||||||
const redditMatch = answerText.match(/【Reddit平台回答】\n?([\s\S]*?)$/)
|
const redditMatch = answerText.match(REPORT_MARKERS.redditAnswer.regex)
|
||||||
|
|
||||||
if (twitterMatch) {
|
if (twitterMatch) {
|
||||||
interview.twitterAnswer = twitterMatch[1].trim()
|
interview.twitterAnswer = twitterMatch[1].trim()
|
||||||
}
|
}
|
||||||
if (redditMatch) {
|
if (redditMatch) {
|
||||||
interview.redditAnswer = redditMatch[1].trim()
|
interview.redditAnswer = redditMatch[1].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for older formats with only a single platform tag.
|
// 平台回退逻辑(兼容旧格式:只有一个平台标记的情况)
|
||||||
if (!twitterMatch && redditMatch) {
|
if (!twitterMatch && redditMatch) {
|
||||||
// Only Reddit replied — copy across as the default display unless the reply is the placeholder text.
|
// 只有 Reddit 回答,仅在非占位文本时复制为默认显示
|
||||||
if (interview.redditAnswer && interview.redditAnswer !== '(该平台未获得回复)') {
|
if (interview.redditAnswer && !REPORT_MARKERS.noReply.is(interview.redditAnswer)) {
|
||||||
interview.twitterAnswer = interview.redditAnswer
|
interview.twitterAnswer = interview.redditAnswer
|
||||||
}
|
}
|
||||||
} else if (twitterMatch && !redditMatch) {
|
} else if (twitterMatch && !redditMatch) {
|
||||||
if (interview.twitterAnswer && interview.twitterAnswer !== '(该平台未获得回复)') {
|
if (interview.twitterAnswer && !REPORT_MARKERS.noReply.is(interview.twitterAnswer)) {
|
||||||
interview.redditAnswer = interview.twitterAnswer
|
interview.redditAnswer = interview.twitterAnswer
|
||||||
}
|
}
|
||||||
} else if (!twitterMatch && !redditMatch) {
|
} else if (!twitterMatch && !redditMatch) {
|
||||||
|
|
@ -859,9 +967,9 @@ const parseInterview = (text) => {
|
||||||
interview.twitterAnswer = answerText
|
interview.twitterAnswer = answerText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract key quotes (supports multiple quote-character styles).
|
// 提取关键引言(兼容多种引号格式)
|
||||||
const quotesMatch = block.match(/\*\*关键引言:\*\*\n([\s\S]*?)(?=\n---|\n####|$)/)
|
const quotesMatch = block.match(REPORT_MARKERS.keyQuotesBlock.regex)
|
||||||
if (quotesMatch) {
|
if (quotesMatch) {
|
||||||
const quotesText = quotesMatch[1]
|
const quotesText = quotesMatch[1]
|
||||||
// Prefer the > "text" form.
|
// Prefer the > "text" form.
|
||||||
|
|
@ -882,8 +990,8 @@ const parseInterview = (text) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Extract the interview summary.
|
// 提取采访摘要
|
||||||
const summaryMatch = text.match(/### 采访摘要与核心观点\n([\s\S]*?)$/)
|
const summaryMatch = text.match(REPORT_MARKERS.interviewSummary.regex)
|
||||||
if (summaryMatch) {
|
if (summaryMatch) {
|
||||||
result.summary = summaryMatch[1].trim()
|
result.summary = summaryMatch[1].trim()
|
||||||
}
|
}
|
||||||
|
|
@ -904,23 +1012,23 @@ const parseQuickSearch = (text) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract the search query.
|
// 提取搜索查询
|
||||||
const queryMatch = text.match(/搜索查询:\s*(.+?)(?:\n|$)/)
|
const queryMatch = text.match(REPORT_MARKERS.searchQuery.regex)
|
||||||
if (queryMatch) result.query = queryMatch[1].trim()
|
if (queryMatch) result.query = queryMatch[1].trim()
|
||||||
|
|
||||||
// Extract the result count.
|
// 提取结果数量
|
||||||
const countMatch = text.match(/找到\s*(\d+)\s*条/)
|
const countMatch = text.match(REPORT_MARKERS.searchCount.regex)
|
||||||
if (countMatch) result.count = parseInt(countMatch[1])
|
if (countMatch) result.count = parseInt(countMatch[1])
|
||||||
|
|
||||||
// Extract related facts in full (no cap).
|
// 提取相关事实 - 完整提取,不限制数量
|
||||||
const factsSection = text.match(/### 相关事实:\n([\s\S]*)$/)
|
const factsSection = text.match(REPORT_MARKERS.relatedFactsHdr.regex)
|
||||||
if (factsSection) {
|
if (factsSection) {
|
||||||
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
|
||||||
result.facts = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
|
result.facts = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort extraction of edge info (if present).
|
// 尝试提取边信息(如果有)
|
||||||
const edgesSection = text.match(/### 相关边:\n([\s\S]*?)(?=\n###|$)/)
|
const edgesSection = text.match(REPORT_MARKERS.relatedEdgesHdr.regex)
|
||||||
if (edgesSection) {
|
if (edgesSection) {
|
||||||
const lines = edgesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
const lines = edgesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||||||
result.edges = lines.map(l => {
|
result.edges = lines.map(l => {
|
||||||
|
|
@ -931,9 +1039,9 @@ const parseQuickSearch = (text) => {
|
||||||
return null
|
return null
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort extraction of node info (if present).
|
// 尝试提取节点信息(如果有)
|
||||||
const nodesSection = text.match(/### 相关节点:\n([\s\S]*?)(?=\n###|$)/)
|
const nodesSection = text.match(REPORT_MARKERS.relatedNodesHdr.regex)
|
||||||
if (nodesSection) {
|
if (nodesSection) {
|
||||||
const lines = nodesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
const lines = nodesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
|
||||||
result.nodes = lines.map(l => {
|
result.nodes = lines.map(l => {
|
||||||
|
|
@ -1284,7 +1392,7 @@ const InterviewDisplay = {
|
||||||
const cleanQuoteText = (text) => {
|
const cleanQuoteText = (text) => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
// Remove leading patterns like "1. ", "2. ", "1、", "(1)", "(1)" etc.
|
// Remove leading patterns like "1. ", "2. ", "1、", "(1)", "(1)" etc.
|
||||||
return text.replace(/^\s*\d+[\.\、\))]\s*/, '').trim()
|
return text.replace(REPORT_MARKERS.numberedListPrefix.regex, '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeIndex = ref(0)
|
const activeIndex = ref(0)
|
||||||
|
|
@ -1321,8 +1429,7 @@ const InterviewDisplay = {
|
||||||
// Detect the "no reply on this platform" placeholder values from the backend.
|
// Detect the "no reply on this platform" placeholder values from the backend.
|
||||||
const isPlaceholderText = (text) => {
|
const isPlaceholderText = (text) => {
|
||||||
if (!text) return true
|
if (!text) return true
|
||||||
const t = text.trim()
|
return REPORT_MARKERS.noReply.is(text.trim())
|
||||||
return t === '(该平台未获得回复)' || t === '(该平台未获得回复)' || t === '[无回复]'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to split a single answer blob into one chunk per question.
|
// Try to split a single answer blob into one chunk per question.
|
||||||
|
|
@ -1336,8 +1443,9 @@ const InterviewDisplay = {
|
||||||
let matches = []
|
let matches = []
|
||||||
let match
|
let match
|
||||||
|
|
||||||
// Try the "问题X:" form first.
|
// 优先尝试 "问题X:" 格式
|
||||||
const cnPattern = /(?:^|[\r\n]+)问题(\d+)[::]\s*/g
|
const cnPattern = REPORT_MARKERS.numberedQuestion.regex
|
||||||
|
cnPattern.lastIndex = 0
|
||||||
while ((match = cnPattern.exec(answerText)) !== null) {
|
while ((match = cnPattern.exec(answerText)) !== null) {
|
||||||
matches.push({
|
matches.push({
|
||||||
num: parseInt(match[1]),
|
num: parseInt(match[1]),
|
||||||
|
|
@ -1361,7 +1469,7 @@ const InterviewDisplay = {
|
||||||
// No numbering (or only one match) — return the whole blob as one answer.
|
// No numbering (or only one match) — return the whole blob as one answer.
|
||||||
if (matches.length <= 1) {
|
if (matches.length <= 1) {
|
||||||
const cleaned = answerText
|
const cleaned = answerText
|
||||||
.replace(/^问题\d+[::]\s*/, '')
|
.replace(REPORT_MARKERS.numberedQuestionStrip.regex, '')
|
||||||
.replace(/^\d+\.\s+/, '')
|
.replace(/^\d+\.\s+/, '')
|
||||||
.trim()
|
.trim()
|
||||||
return [cleaned || answerText]
|
return [cleaned || answerText]
|
||||||
|
|
@ -1461,7 +1569,7 @@ const InterviewDisplay = {
|
||||||
|
|
||||||
// Selection Reason
|
// Selection Reason
|
||||||
props.result.interviews[activeIndex.value]?.selectionReason && h('div', { class: 'selection-reason' }, [
|
props.result.interviews[activeIndex.value]?.selectionReason && h('div', { class: 'selection-reason' }, [
|
||||||
h('div', { class: 'reason-label' }, '选择理由'),
|
h('div', { class: 'reason-label' }, t('step4.selectionReason')),
|
||||||
h('div', { class: 'reason-content' }, props.result.interviews[activeIndex.value].selectionReason)
|
h('div', { class: 'reason-content' }, props.result.interviews[activeIndex.value].selectionReason)
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
|
@ -1770,8 +1878,8 @@ const activeStep = computed(() => {
|
||||||
const doneSteps = steps.filter(s => s.status === 'done')
|
const doneSteps = steps.filter(s => s.status === 'done')
|
||||||
if (doneSteps.length > 0) return doneSteps[doneSteps.length - 1]
|
if (doneSteps.length > 0) return doneSteps[doneSteps.length - 1]
|
||||||
|
|
||||||
// Otherwise return the first step in the list.
|
// 否则返回第一个步骤
|
||||||
return steps[0] || { noLabel: '--', title: '等待开始', status: 'todo', meta: '' }
|
return steps[0] || { noLabel: '--', title: t('step4.awaitingStart'), status: 'todo', meta: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const workflowSteps = computed(() => {
|
const workflowSteps = computed(() => {
|
||||||
|
|
@ -2002,9 +2110,9 @@ const getActionLabel = (action) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLogLevelClass = (log) => {
|
const getLogLevelClass = (log) => {
|
||||||
if (log.includes('ERROR') || log.includes('错误')) return 'error'
|
if (REPORT_MARKERS.logSeverity.isError(log)) return 'error'
|
||||||
if (log.includes('WARNING') || log.includes('警告')) return 'warning'
|
if (REPORT_MARKERS.logSeverity.isWarning(log)) return 'warning'
|
||||||
// INFO uses the default color and is intentionally not marked as success.
|
// INFO 使用默认颜色,不标记为 success
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2093,8 +2201,8 @@ const extractFinalContent = (response) => {
|
||||||
return finalAnswerMatch[1].trim()
|
return finalAnswerMatch[1].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for content after the Chinese "最终答案:" marker.
|
// 尝试找 最终答案: 后面的内容
|
||||||
const chineseFinalMatch = response.match(/最终答案[::]\s*\n*([\s\S]*)$/i)
|
const chineseFinalMatch = response.match(REPORT_MARKERS.finalAnswerCn.regex)
|
||||||
if (chineseFinalMatch) {
|
if (chineseFinalMatch) {
|
||||||
return chineseFinalMatch[1].trim()
|
return chineseFinalMatch[1].trim()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -712,15 +712,23 @@ const sendToAgent = async (message) => {
|
||||||
|
|
||||||
addLog(t('log.sendToAgent', { name: selectedAgent.value.username, message: message.substring(0, 50) }))
|
addLog(t('log.sendToAgent', { name: selectedAgent.value.username, message: message.substring(0, 50) }))
|
||||||
|
|
||||||
// Build prompt with chat history
|
// Build prompt with chat history. The role labels, separator, and prefix
|
||||||
|
// wording follow the active locale; production Chinese behaviour is
|
||||||
|
// unchanged because zh.json carries the original phrasing byte-for-byte
|
||||||
|
// (including the full-width colon in chatHistoryRoleLine).
|
||||||
let prompt = message
|
let prompt = message
|
||||||
if (chatHistory.value.length > 1) {
|
if (chatHistory.value.length > 1) {
|
||||||
|
const rolePrompter = t('step5.chatRolePrompter')
|
||||||
|
const roleYou = t('step5.chatRoleYou')
|
||||||
const historyContext = chatHistory.value
|
const historyContext = chatHistory.value
|
||||||
.filter(msg => msg.content !== message)
|
.filter(msg => msg.content !== message)
|
||||||
.slice(-6)
|
.slice(-6)
|
||||||
.map(msg => `${msg.role === 'user' ? '提问者' : '你'}:${msg.content}`)
|
.map(msg => t('step5.chatHistoryRoleLine', {
|
||||||
|
role: msg.role === 'user' ? rolePrompter : roleYou,
|
||||||
|
content: msg.content,
|
||||||
|
}))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
prompt = `以下是我们之前的对话:\n${historyContext}\n\n现在我的新问题是:${message}`
|
prompt = t('step5.chatHistoryPrompt', { history: historyContext, message })
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await interviewAgents({
|
const res = await interviewAgents({
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<!-- Center step indicator -->
|
<!-- Center step indicator -->
|
||||||
<div class="nav-center">
|
<div class="nav-center">
|
||||||
<div class="step-badge">STEP 01</div>
|
<div class="step-badge">STEP 01</div>
|
||||||
<div class="step-name">图谱构建</div>
|
<div class="step-name">{{ t('process.stepName') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-status">
|
<div class="nav-status">
|
||||||
|
|
@ -23,20 +23,20 @@
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="header-deco">◆</span>
|
<span class="header-deco">◆</span>
|
||||||
<span class="header-title">实时知识图谱</span>
|
<span class="header-title">{{ t('process.realtimeKnowledgeGraph') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<template v-if="graphData">
|
<template v-if="graphData">
|
||||||
<span class="stat-item">{{ graphData.node_count || graphData.nodes?.length || 0 }} 节点</span>
|
<span class="stat-item">{{ graphData.node_count || graphData.nodes?.length || 0 }} {{ t('process.nodes') }}</span>
|
||||||
<span class="stat-divider">|</span>
|
<span class="stat-divider">|</span>
|
||||||
<span class="stat-item">{{ graphData.edge_count || graphData.edges?.length || 0 }} 关系</span>
|
<span class="stat-item">{{ graphData.edge_count || graphData.edges?.length || 0 }} {{ t('process.relations') }}</span>
|
||||||
<span class="stat-divider">|</span>
|
<span class="stat-divider">|</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="action-btn" @click="refreshGraph" :disabled="graphLoading" title="刷新图谱">
|
<button class="action-btn" @click="refreshGraph" :disabled="graphLoading" :title="t('graph.refreshGraph')">
|
||||||
<span class="icon-refresh" :class="{ 'spinning': graphLoading }">↻</span>
|
<span class="icon-refresh" :class="{ 'spinning': graphLoading }">↻</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" @click="toggleFullScreen" :title="isFullScreen ? '退出全屏' : '全屏显示'">
|
<button class="action-btn" @click="toggleFullScreen" :title="isFullScreen ? t('process.exitFullscreen') : t('process.enterFullscreen')">
|
||||||
<span class="icon-fullscreen">{{ isFullScreen ? '↙' : '↗' }}</span>
|
<span class="icon-fullscreen">{{ isFullScreen ? '↙' : '↗' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
<!-- Build-in-progress banner -->
|
<!-- Build-in-progress banner -->
|
||||||
<div v-if="currentPhase === 1" class="graph-building-hint">
|
<div v-if="currentPhase === 1" class="graph-building-hint">
|
||||||
<span class="building-dot"></span>
|
<span class="building-dot"></span>
|
||||||
实时更新中...
|
{{ t('process.realtimeUpdating') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Node / edge detail panel -->
|
<!-- Node / edge detail panel -->
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
<div class="loading-ring"></div>
|
<div class="loading-ring"></div>
|
||||||
<div class="loading-ring"></div>
|
<div class="loading-ring"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-text">图谱数据加载中...</p>
|
<p class="loading-text">{{ t('graph.graphDataLoading') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waiting for build -->
|
<!-- Waiting for build -->
|
||||||
|
|
@ -189,8 +189,8 @@
|
||||||
<line x1="50" y1="72" x2="74" y2="66" stroke="#000" stroke-width="1"/>
|
<line x1="50" y1="72" x2="74" y2="66" stroke="#000" stroke-width="1"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="waiting-text">等待本体生成</p>
|
<p class="waiting-text">{{ t('process.waitingOntologyTitle') }}</p>
|
||||||
<p class="waiting-hint">生成完成后将自动开始构建图谱</p>
|
<p class="waiting-hint">{{ t('process.waitingOntologyHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Build started but no data yet -->
|
<!-- Build started but no data yet -->
|
||||||
|
|
@ -200,8 +200,8 @@
|
||||||
<div class="loading-ring"></div>
|
<div class="loading-ring"></div>
|
||||||
<div class="loading-ring"></div>
|
<div class="loading-ring"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="waiting-text">图谱构建中</p>
|
<p class="waiting-text">{{ t('process.graphBuildingTitle') }}</p>
|
||||||
<p class="waiting-hint">数据即将显示...</p>
|
<p class="waiting-hint">{{ t('process.graphBuildingHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error state -->
|
<!-- Error state -->
|
||||||
|
|
@ -225,7 +225,7 @@
|
||||||
<div class="right-panel" :class="{ 'hidden': isFullScreen }">
|
<div class="right-panel" :class="{ 'hidden': isFullScreen }">
|
||||||
<div class="panel-header dark-header">
|
<div class="panel-header dark-header">
|
||||||
<span class="header-icon">▣</span>
|
<span class="header-icon">▣</span>
|
||||||
<span class="header-title">构建流程</span>
|
<span class="header-title">{{ t('process.buildFlow') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="process-content">
|
<div class="process-content">
|
||||||
|
|
@ -234,7 +234,7 @@
|
||||||
<div class="phase-header">
|
<div class="phase-header">
|
||||||
<span class="phase-num">01</span>
|
<span class="phase-num">01</span>
|
||||||
<div class="phase-info">
|
<div class="phase-info">
|
||||||
<div class="phase-title">本体生成</div>
|
<div class="phase-title">{{ t('process.ontologyGenerationLabel') }}</div>
|
||||||
<div class="phase-api">/api/graph/ontology/generate</div>
|
<div class="phase-api">/api/graph/ontology/generate</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="phase-status" :class="getPhaseStatusClass(0)">
|
<span class="phase-status" :class="getPhaseStatusClass(0)">
|
||||||
|
|
@ -244,15 +244,15 @@
|
||||||
|
|
||||||
<div class="phase-detail">
|
<div class="phase-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<div class="detail-label">接口说明</div>
|
<div class="detail-label">{{ t('process.interfaceNote') }}</div>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
上传文档后,LLM分析文档内容,自动生成适合舆论模拟的本体结构(实体类型 + 关系类型)
|
{{ t('process.ontologyDescriptionLong') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ontology generation progress -->
|
<!-- 本体生成进度 -->
|
||||||
<div class="detail-section" v-if="ontologyProgress && currentPhase === 0">
|
<div class="detail-section" v-if="ontologyProgress && currentPhase === 0">
|
||||||
<div class="detail-label">生成进度</div>
|
<div class="detail-label">{{ t('process.generationProgress') }}</div>
|
||||||
<div class="ontology-progress">
|
<div class="ontology-progress">
|
||||||
<div class="progress-spinner"></div>
|
<div class="progress-spinner"></div>
|
||||||
<span class="progress-text">{{ ontologyProgress.message }}</span>
|
<span class="progress-text">{{ ontologyProgress.message }}</span>
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
|
|
||||||
<!-- Generated ontology summary -->
|
<!-- Generated ontology summary -->
|
||||||
<div class="detail-section" v-if="projectData?.ontology">
|
<div class="detail-section" v-if="projectData?.ontology">
|
||||||
<div class="detail-label">生成的实体类型 ({{ projectData.ontology.entity_types?.length || 0 }})</div>
|
<div class="detail-label">{{ t('process.generatedEntityTypes') }} ({{ projectData.ontology.entity_types?.length || 0 }})</div>
|
||||||
<div class="entity-tags">
|
<div class="entity-tags">
|
||||||
<span
|
<span
|
||||||
v-for="entity in projectData.ontology.entity_types"
|
v-for="entity in projectData.ontology.entity_types"
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-section" v-if="projectData?.ontology">
|
<div class="detail-section" v-if="projectData?.ontology">
|
||||||
<div class="detail-label">生成的关系类型 ({{ projectData.ontology.relation_types?.length || 0 }})</div>
|
<div class="detail-label">{{ t('process.generatedRelationTypes') }} ({{ projectData.ontology.relation_types?.length || 0 }})</div>
|
||||||
<div class="relation-list">
|
<div class="relation-list">
|
||||||
<div
|
<div
|
||||||
v-for="(rel, idx) in projectData.ontology.relation_types?.slice(0, 5) || []"
|
v-for="(rel, idx) in projectData.ontology.relation_types?.slice(0, 5) || []"
|
||||||
|
|
@ -288,14 +288,14 @@
|
||||||
<span class="rel-target">{{ rel.target_type }}</span>
|
<span class="rel-target">{{ rel.target_type }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(projectData.ontology.relation_types?.length || 0) > 5" class="relation-more">
|
<div v-if="(projectData.ontology.relation_types?.length || 0) > 5" class="relation-more">
|
||||||
+{{ projectData.ontology.relation_types.length - 5 }} 更多关系...
|
{{ t('process.moreRelations', { count: projectData.ontology.relation_types.length - 5 }) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waiting state -->
|
<!-- Waiting state -->
|
||||||
<div class="detail-section waiting-state" v-if="!projectData?.ontology && currentPhase === 0 && !ontologyProgress">
|
<div class="detail-section waiting-state" v-if="!projectData?.ontology && currentPhase === 0 && !ontologyProgress">
|
||||||
<div class="waiting-hint">等待本体生成...</div>
|
<div class="waiting-hint">{{ t('process.waitingOntologyDots') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -305,7 +305,7 @@
|
||||||
<div class="phase-header">
|
<div class="phase-header">
|
||||||
<span class="phase-num">02</span>
|
<span class="phase-num">02</span>
|
||||||
<div class="phase-info">
|
<div class="phase-info">
|
||||||
<div class="phase-title">图谱构建</div>
|
<div class="phase-title">{{ t('process.graphBuildSection') }}</div>
|
||||||
<div class="phase-api">/api/graph/build</div>
|
<div class="phase-api">/api/graph/build</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="phase-status" :class="getPhaseStatusClass(1)">
|
<span class="phase-status" :class="getPhaseStatusClass(1)">
|
||||||
|
|
@ -315,20 +315,20 @@
|
||||||
|
|
||||||
<div class="phase-detail">
|
<div class="phase-detail">
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<div class="detail-label">接口说明</div>
|
<div class="detail-label">{{ t('process.interfaceNote') }}</div>
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
基于生成的本体,将文档分块后调用 Zep API 构建知识图谱,提取实体和关系
|
{{ t('process.graphBuildDescription') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waiting for ontology to finish -->
|
<!-- 等待本体完成 -->
|
||||||
<div class="detail-section waiting-state" v-if="currentPhase < 1">
|
<div class="detail-section waiting-state" v-if="currentPhase < 1">
|
||||||
<div class="waiting-hint">等待本体生成完成...</div>
|
<div class="waiting-hint">{{ t('process.waitingOntologyComplete') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Build progress -->
|
<!-- 构建进度 -->
|
||||||
<div class="detail-section" v-if="buildProgress && currentPhase >= 1">
|
<div class="detail-section" v-if="buildProgress && currentPhase >= 1">
|
||||||
<div class="detail-label">构建进度</div>
|
<div class="detail-label">{{ t('process.buildProgressLabel') }}</div>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
<div class="progress-fill" :style="{ width: buildProgress.progress + '%' }"></div>
|
<div class="progress-fill" :style="{ width: buildProgress.progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -339,19 +339,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-section" v-if="graphData">
|
<div class="detail-section" v-if="graphData">
|
||||||
<div class="detail-label">构建结果</div>
|
<div class="detail-label">{{ t('process.buildResultLabel') }}</div>
|
||||||
<div class="build-result">
|
<div class="build-result">
|
||||||
<div class="result-item">
|
<div class="result-item">
|
||||||
<span class="result-value">{{ graphData.node_count }}</span>
|
<span class="result-value">{{ graphData.node_count }}</span>
|
||||||
<span class="result-label">实体节点</span>
|
<span class="result-label">{{ t('process.entityNodes') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-item">
|
<div class="result-item">
|
||||||
<span class="result-value">{{ graphData.edge_count }}</span>
|
<span class="result-value">{{ graphData.edge_count }}</span>
|
||||||
<span class="result-label">关系边</span>
|
<span class="result-label">{{ t('process.relationEdges') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-item">
|
<div class="result-item">
|
||||||
<span class="result-value">{{ entityTypes.length }}</span>
|
<span class="result-value">{{ entityTypes.length }}</span>
|
||||||
<span class="result-label">实体类型</span>
|
<span class="result-label">{{ t('process.entityTypesLabel') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -363,8 +363,8 @@
|
||||||
<div class="phase-header">
|
<div class="phase-header">
|
||||||
<span class="phase-num">03</span>
|
<span class="phase-num">03</span>
|
||||||
<div class="phase-info">
|
<div class="phase-info">
|
||||||
<div class="phase-title">构建完成</div>
|
<div class="phase-title">{{ t('process.buildCompleteSection') }}</div>
|
||||||
<div class="phase-api">准备进入下一步骤</div>
|
<div class="phase-api">{{ t('process.buildCompleteHint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="phase-status" :class="getPhaseStatusClass(2)">
|
<span class="phase-status" :class="getPhaseStatusClass(2)">
|
||||||
{{ getPhaseStatusText(2) }}
|
{{ getPhaseStatusText(2) }}
|
||||||
|
|
@ -375,7 +375,7 @@
|
||||||
<!-- Next-step button -->
|
<!-- Next-step button -->
|
||||||
<div class="next-step-section" v-if="currentPhase >= 2">
|
<div class="next-step-section" v-if="currentPhase >= 2">
|
||||||
<button class="next-step-btn" @click="goToNextStep" :disabled="currentPhase < 2">
|
<button class="next-step-btn" @click="goToNextStep" :disabled="currentPhase < 2">
|
||||||
进入环境搭建
|
{{ t('process.enterEnvSetup') }}
|
||||||
<span class="btn-arrow">→</span>
|
<span class="btn-arrow">→</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -385,23 +385,23 @@
|
||||||
<div class="project-panel">
|
<div class="project-panel">
|
||||||
<div class="project-header">
|
<div class="project-header">
|
||||||
<span class="project-icon">◇</span>
|
<span class="project-icon">◇</span>
|
||||||
<span class="project-title">项目信息</span>
|
<span class="project-title">{{ t('process.projectInfo') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-details" v-if="projectData">
|
<div class="project-details" v-if="projectData">
|
||||||
<div class="project-item">
|
<div class="project-item">
|
||||||
<span class="item-label">项目名称</span>
|
<span class="item-label">{{ t('process.projectName') }}</span>
|
||||||
<span class="item-value">{{ projectData.name }}</span>
|
<span class="item-value">{{ projectData.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-item">
|
<div class="project-item">
|
||||||
<span class="item-label">项目ID</span>
|
<span class="item-label">{{ t('process.projectId') }}</span>
|
||||||
<span class="item-value code">{{ projectData.project_id }}</span>
|
<span class="item-value code">{{ projectData.project_id }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-item" v-if="projectData.graph_id">
|
<div class="project-item" v-if="projectData.graph_id">
|
||||||
<span class="item-label">图谱ID</span>
|
<span class="item-label">{{ t('process.graphId') }}</span>
|
||||||
<span class="item-value code">{{ projectData.graph_id }}</span>
|
<span class="item-value code">{{ projectData.graph_id }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-item">
|
<div class="project-item">
|
||||||
<span class="item-label">模拟需求</span>
|
<span class="item-label">{{ t('process.simulationRequirement') }}</span>
|
||||||
<span class="item-value">{{ projectData.simulation_requirement || '-' }}</span>
|
<span class="item-value">{{ projectData.simulation_requirement || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -414,10 +414,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
|
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
|
||||||
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
|
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
@ -449,11 +451,11 @@ const statusClass = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
if (error.value) return '构建失败'
|
if (error.value) return t('process.statusBuildFailed')
|
||||||
if (currentPhase.value >= 2) return '构建完成'
|
if (currentPhase.value >= 2) return t('process.statusBuildComplete')
|
||||||
if (currentPhase.value === 1) return '图谱构建中'
|
if (currentPhase.value === 1) return t('process.statusGraphBuilding')
|
||||||
if (currentPhase.value === 0) return '本体生成中'
|
if (currentPhase.value === 0) return t('process.statusOntologyInProgress')
|
||||||
return '初始化中'
|
return t('process.statusInitializing')
|
||||||
})
|
})
|
||||||
|
|
||||||
const entityTypes = computed(() => {
|
const entityTypes = computed(() => {
|
||||||
|
|
@ -478,8 +480,8 @@ const goHome = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToNextStep = () => {
|
const goToNextStep = () => {
|
||||||
// TODO(#9): Wire up the transition into Step 2 (Environment Setup).
|
// TODO: 进入环境搭建步骤
|
||||||
alert('环境搭建功能开发中...')
|
alert(t('process.envSetupComingSoon'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleFullScreen = () => {
|
const toggleFullScreen = () => {
|
||||||
|
|
@ -533,14 +535,14 @@ const getPhaseStatusClass = (phase) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPhaseStatusText = (phase) => {
|
const getPhaseStatusText = (phase) => {
|
||||||
if (currentPhase.value > phase) return '已完成'
|
if (currentPhase.value > phase) return t('process.stepCompleted')
|
||||||
if (currentPhase.value === phase) {
|
if (currentPhase.value === phase) {
|
||||||
if (phase === 1 && buildProgress.value) {
|
if (phase === 1 && buildProgress.value) {
|
||||||
return `${buildProgress.value.progress}%`
|
return `${buildProgress.value.progress}%`
|
||||||
}
|
}
|
||||||
return '进行中'
|
return t('process.stepInProgress')
|
||||||
}
|
}
|
||||||
return '等待中'
|
return t('process.stepWaiting')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize: either create a new project from the pending-upload store, or load an existing one by id.
|
// Initialize: either create a new project from the pending-upload store, or load an existing one by id.
|
||||||
|
|
@ -560,15 +562,15 @@ const handleNewProject = async () => {
|
||||||
const pending = getPendingUpload()
|
const pending = getPendingUpload()
|
||||||
|
|
||||||
if (!pending.isPending || pending.files.length === 0) {
|
if (!pending.isPending || pending.files.length === 0) {
|
||||||
error.value = '没有待上传的文件,请返回首页重新操作'
|
error.value = t('process.noFilesError')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
currentPhase.value = 0 // Ontology-generation phase.
|
currentPhase.value = 0 // 本体生成阶段
|
||||||
ontologyProgress.value = { message: '正在上传文件并分析文档...' }
|
ontologyProgress.value = { message: t('process.uploadingFiles') }
|
||||||
|
|
||||||
const formDataObj = new FormData()
|
const formDataObj = new FormData()
|
||||||
pending.files.forEach(file => {
|
pending.files.forEach(file => {
|
||||||
|
|
@ -595,11 +597,11 @@ const handleNewProject = async () => {
|
||||||
// Kick off the graph build automatically.
|
// Kick off the graph build automatically.
|
||||||
await startBuildGraph()
|
await startBuildGraph()
|
||||||
} else {
|
} else {
|
||||||
error.value = response.error || '本体生成失败'
|
error.value = response.error || t('process.ontologyGenerationFailed')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Handle new project error:', err)
|
console.error('Handle new project error:', err)
|
||||||
error.value = '项目初始化失败: ' + (err.message || '未知错误')
|
error.value = t('process.projectInitFailed', { error: err.message || t('common.unknownError') })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -631,11 +633,11 @@ const loadProject = async () => {
|
||||||
await loadGraph(response.data.graph_id)
|
await loadGraph(response.data.graph_id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = response.error || '加载项目失败'
|
error.value = response.error || t('process.loadProjectFailed')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Load project error:', err)
|
console.error('Load project error:', err)
|
||||||
error.value = '加载项目失败: ' + (err.message || '未知错误')
|
error.value = t('process.loadProjectFailedDetail', { error: err.message || t('common.unknownError') })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +656,7 @@ const updatePhaseByStatus = (status) => {
|
||||||
currentPhase.value = 2
|
currentPhase.value = 2
|
||||||
break
|
break
|
||||||
case 'failed':
|
case 'failed':
|
||||||
error.value = projectData.value?.error || '处理失败'
|
error.value = projectData.value?.error || t('process.processingFailed')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -664,26 +666,29 @@ const startBuildGraph = async () => {
|
||||||
currentPhase.value = 1
|
currentPhase.value = 1
|
||||||
buildProgress.value = {
|
buildProgress.value = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
message: '正在启动图谱构建...'
|
message: t('process.graphBuildStarting')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await buildGraph({ project_id: currentProjectId.value })
|
const response = await buildGraph({ project_id: currentProjectId.value })
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
buildProgress.value.message = '图谱构建任务已启动...'
|
buildProgress.value.message = t('process.graphBuildTaskStarted')
|
||||||
|
|
||||||
|
// 保存 task_id 用于轮询
|
||||||
const taskId = response.data.task_id
|
const taskId = response.data.task_id
|
||||||
|
|
||||||
// Two independent polling loops: graph data refresh AND task-status polling.
|
// 启动图谱数据轮询(独立于任务状态轮询)
|
||||||
startGraphPolling()
|
startGraphPolling()
|
||||||
|
|
||||||
|
// 启动任务状态轮询
|
||||||
startPollingTask(taskId)
|
startPollingTask(taskId)
|
||||||
} else {
|
} else {
|
||||||
error.value = response.error || '启动图谱构建失败'
|
error.value = response.error || t('process.graphBuildStartFailed')
|
||||||
buildProgress.value = null
|
buildProgress.value = null
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Build graph error:', err)
|
console.error('Build graph error:', err)
|
||||||
error.value = '启动图谱构建失败: ' + (err.message || '未知错误')
|
error.value = t('process.graphBuildStartFailedDetail', { error: err.message || t('common.unknownError') })
|
||||||
buildProgress.value = null
|
buildProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -760,7 +765,7 @@ const pollTaskStatus = async (taskId) => {
|
||||||
|
|
||||||
buildProgress.value = {
|
buildProgress.value = {
|
||||||
progress: task.progress || 0,
|
progress: task.progress || 0,
|
||||||
message: task.message || '处理中...'
|
message: task.message || t('process.graphProcessing')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Task status:', task.status, 'Progress:', task.progress)
|
console.log('Task status:', task.status, 'Progress:', task.progress)
|
||||||
|
|
@ -775,7 +780,7 @@ const pollTaskStatus = async (taskId) => {
|
||||||
// Update the progress display to a "complete" state.
|
// Update the progress display to a "complete" state.
|
||||||
buildProgress.value = {
|
buildProgress.value = {
|
||||||
progress: 100,
|
progress: 100,
|
||||||
message: '构建完成,正在加载图谱...'
|
message: t('process.graphBuildLoadingFull')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the project so we have a fresh graph_id.
|
// Reload the project so we have a fresh graph_id.
|
||||||
|
|
@ -794,7 +799,7 @@ const pollTaskStatus = async (taskId) => {
|
||||||
} else if (task.status === 'failed') {
|
} else if (task.status === 'failed') {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
stopGraphPolling()
|
stopGraphPolling()
|
||||||
error.value = '图谱构建失败: ' + (task.error || '未知错误')
|
error.value = t('process.graphBuildFailedDetail', { error: task.error || t('common.unknownError') })
|
||||||
buildProgress.value = null
|
buildProgress.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -869,7 +874,7 @@ const renderGraph = () => {
|
||||||
.attr('y', height / 2)
|
.attr('y', height / 2)
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('fill', '#999')
|
.attr('fill', '#999')
|
||||||
.text('等待图谱数据...')
|
.text(t('process.waitingGraphData'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -881,7 +886,7 @@ const renderGraph = () => {
|
||||||
|
|
||||||
const nodes = nodesData.map(n => ({
|
const nodes = nodesData.map(n => ({
|
||||||
id: n.uuid,
|
id: n.uuid,
|
||||||
name: n.name || '未命名',
|
name: n.name || t('process.fallbackNodeName'),
|
||||||
type: n.labels?.find(l => l !== 'Entity' && l !== 'Node') || 'Entity',
|
type: n.labels?.find(l => l !== 'Entity' && l !== 'Node') || 'Entity',
|
||||||
rawData: n // Keep the original data on the simulation node.
|
rawData: n // Keep the original data on the simulation node.
|
||||||
}))
|
}))
|
||||||
|
|
@ -897,8 +902,8 @@ const renderGraph = () => {
|
||||||
type: e.fact_type || e.name || 'RELATED_TO',
|
type: e.fact_type || e.name || 'RELATED_TO',
|
||||||
rawData: {
|
rawData: {
|
||||||
...e,
|
...e,
|
||||||
source_name: nodeMap[e.source_node_uuid]?.name || '未知',
|
source_name: nodeMap[e.source_node_uuid]?.name || t('common.unknown'),
|
||||||
target_name: nodeMap[e.target_node_uuid]?.name || '未知'
|
target_name: nodeMap[e.target_node_uuid]?.name || t('common.unknown')
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,69 @@
|
||||||
"Deep Interaction"
|
"Deep Interaction"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"process": {
|
||||||
|
"stepName": "Graph Build",
|
||||||
|
"realtimeKnowledgeGraph": "Real-time Knowledge Graph",
|
||||||
|
"nodes": "nodes",
|
||||||
|
"relations": "relations",
|
||||||
|
"exitFullscreen": "Exit fullscreen",
|
||||||
|
"enterFullscreen": "Fullscreen",
|
||||||
|
"graphBuildingTitle": "Graph build in progress",
|
||||||
|
"graphBuildingHint": "Data will appear shortly...",
|
||||||
|
"waitingOntologyTitle": "Waiting for ontology generation",
|
||||||
|
"waitingOntologyHint": "Graph build will start automatically once ontology generation completes",
|
||||||
|
"buildFlow": "Build flow",
|
||||||
|
"ontologyGenerationLabel": "Ontology generation",
|
||||||
|
"interfaceNote": "Interface",
|
||||||
|
"ontologyDescriptionLong": "Once documents are uploaded, the LLM analyses their content and automatically generates an ontology structure suitable for opinion simulation (entity types + relation types).",
|
||||||
|
"generationProgress": "Generation progress",
|
||||||
|
"generatedEntityTypes": "Generated entity types",
|
||||||
|
"generatedRelationTypes": "Generated relation types",
|
||||||
|
"moreRelations": "+{count} more relations...",
|
||||||
|
"waitingOntologyDots": "Waiting for ontology generation...",
|
||||||
|
"graphBuildSection": "Graph build",
|
||||||
|
"graphBuildDescription": "Using the generated ontology, chunks the documents and calls the Zep API to build the knowledge graph, extracting entities and relations.",
|
||||||
|
"waitingOntologyComplete": "Waiting for ontology generation to finish...",
|
||||||
|
"buildProgressLabel": "Build progress",
|
||||||
|
"buildResultLabel": "Build result",
|
||||||
|
"entityNodes": "Entity nodes",
|
||||||
|
"relationEdges": "Relation edges",
|
||||||
|
"entityTypesLabel": "Entity types",
|
||||||
|
"buildCompleteSection": "Build complete",
|
||||||
|
"buildCompleteHint": "Ready to proceed to the next step",
|
||||||
|
"enterEnvSetup": "Enter environment setup",
|
||||||
|
"projectInfo": "Project info",
|
||||||
|
"projectName": "Project name",
|
||||||
|
"projectId": "Project ID",
|
||||||
|
"graphId": "Graph ID",
|
||||||
|
"simulationRequirement": "Simulation requirement",
|
||||||
|
"statusBuildFailed": "Build failed",
|
||||||
|
"statusBuildComplete": "Build complete",
|
||||||
|
"statusGraphBuilding": "Graph build in progress",
|
||||||
|
"statusOntologyInProgress": "Ontology generation in progress",
|
||||||
|
"statusInitializing": "Initializing",
|
||||||
|
"envSetupComingSoon": "Environment setup feature coming soon...",
|
||||||
|
"stepCompleted": "Completed",
|
||||||
|
"stepInProgress": "In progress",
|
||||||
|
"stepWaiting": "Waiting",
|
||||||
|
"noFilesError": "No pending uploads. Please return to the home page and start over.",
|
||||||
|
"uploadingFiles": "Uploading files and analysing documents...",
|
||||||
|
"ontologyGenerationFailed": "Ontology generation failed",
|
||||||
|
"projectInitFailed": "Project initialization failed: {error}",
|
||||||
|
"loadProjectFailed": "Failed to load project",
|
||||||
|
"loadProjectFailedDetail": "Failed to load project: {error}",
|
||||||
|
"processingFailed": "Processing failed",
|
||||||
|
"graphBuildStarting": "Starting graph build...",
|
||||||
|
"graphBuildTaskStarted": "Graph build task started...",
|
||||||
|
"graphBuildStartFailed": "Failed to start graph build",
|
||||||
|
"graphBuildStartFailedDetail": "Failed to start graph build: {error}",
|
||||||
|
"graphProcessing": "Processing...",
|
||||||
|
"graphBuildLoadingFull": "Build complete; loading graph...",
|
||||||
|
"graphBuildFailedDetail": "Graph build failed: {error}",
|
||||||
|
"waitingGraphData": "Waiting for graph data...",
|
||||||
|
"fallbackNodeName": "Unnamed",
|
||||||
|
"realtimeUpdating": "Updating in real time..."
|
||||||
|
},
|
||||||
"step1": {
|
"step1": {
|
||||||
"ontologyGeneration": "Ontology Generation",
|
"ontologyGeneration": "Ontology Generation",
|
||||||
"ontologyCompleted": "Completed",
|
"ontologyCompleted": "Completed",
|
||||||
|
|
@ -211,7 +274,8 @@
|
||||||
"forceStopSuccess": "Simulation force stopped",
|
"forceStopSuccess": "Simulation force stopped",
|
||||||
"forceStopFailed": "Force stop failed: {error}",
|
"forceStopFailed": "Force stop failed: {error}",
|
||||||
"startGenerateReportBtn": "Generate Report",
|
"startGenerateReportBtn": "Generate Report",
|
||||||
"generatingReportBtn": "Starting..."
|
"generatingReportBtn": "Starting...",
|
||||||
|
"startFailedFallback": "Failed to start"
|
||||||
},
|
},
|
||||||
"step4": {
|
"step4": {
|
||||||
"generatingSection": "Generating {title}...",
|
"generatingSection": "Generating {title}...",
|
||||||
|
|
@ -253,7 +317,9 @@
|
||||||
"panelRelatedEdges": "Related Edges",
|
"panelRelatedEdges": "Related Edges",
|
||||||
"panelRelatedNodes": "Related Nodes",
|
"panelRelatedNodes": "Related Nodes",
|
||||||
"world1": "World 1",
|
"world1": "World 1",
|
||||||
"world2": "World 2"
|
"world2": "World 2",
|
||||||
|
"selectionReason": "Selection reason",
|
||||||
|
"awaitingStart": "Awaiting start"
|
||||||
},
|
},
|
||||||
"step5": {
|
"step5": {
|
||||||
"interactiveTools": "Interactive Tools",
|
"interactiveTools": "Interactive Tools",
|
||||||
|
|
@ -288,7 +354,11 @@
|
||||||
"errorOccurred": "Sorry, an error occurred: {error}",
|
"errorOccurred": "Sorry, an error occurred: {error}",
|
||||||
"noResponse": "No response",
|
"noResponse": "No response",
|
||||||
"requestFailed": "Request failed",
|
"requestFailed": "Request failed",
|
||||||
"selectAgentFirst": "Please select a simulated individual first"
|
"selectAgentFirst": "Please select a simulated individual first",
|
||||||
|
"chatRolePrompter": "Questioner",
|
||||||
|
"chatRoleYou": "You",
|
||||||
|
"chatHistoryRoleLine": "{role}: {content}",
|
||||||
|
"chatHistoryPrompt": "Here is our previous conversation:\n{history}\n\nMy new question is: {message}"
|
||||||
},
|
},
|
||||||
"graph": {
|
"graph": {
|
||||||
"panelTitle": "Graph Relationship Visualization",
|
"panelTitle": "Graph Relationship Visualization",
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,69 @@
|
||||||
"深度互动"
|
"深度互动"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"process": {
|
||||||
|
"stepName": "图谱构建",
|
||||||
|
"realtimeKnowledgeGraph": "实时知识图谱",
|
||||||
|
"nodes": "节点",
|
||||||
|
"relations": "关系",
|
||||||
|
"exitFullscreen": "退出全屏",
|
||||||
|
"enterFullscreen": "全屏显示",
|
||||||
|
"graphBuildingTitle": "图谱构建中",
|
||||||
|
"graphBuildingHint": "数据即将显示...",
|
||||||
|
"waitingOntologyTitle": "等待本体生成",
|
||||||
|
"waitingOntologyHint": "生成完成后将自动开始构建图谱",
|
||||||
|
"buildFlow": "构建流程",
|
||||||
|
"ontologyGenerationLabel": "本体生成",
|
||||||
|
"interfaceNote": "接口说明",
|
||||||
|
"ontologyDescriptionLong": "上传文档后,LLM分析文档内容,自动生成适合舆论模拟的本体结构(实体类型 + 关系类型)",
|
||||||
|
"generationProgress": "生成进度",
|
||||||
|
"generatedEntityTypes": "生成的实体类型",
|
||||||
|
"generatedRelationTypes": "生成的关系类型",
|
||||||
|
"moreRelations": "+{count} 更多关系...",
|
||||||
|
"waitingOntologyDots": "等待本体生成...",
|
||||||
|
"graphBuildSection": "图谱构建",
|
||||||
|
"graphBuildDescription": "基于生成的本体,将文档分块后调用 Zep API 构建知识图谱,提取实体和关系",
|
||||||
|
"waitingOntologyComplete": "等待本体生成完成...",
|
||||||
|
"buildProgressLabel": "构建进度",
|
||||||
|
"buildResultLabel": "构建结果",
|
||||||
|
"entityNodes": "实体节点",
|
||||||
|
"relationEdges": "关系边",
|
||||||
|
"entityTypesLabel": "实体类型",
|
||||||
|
"buildCompleteSection": "构建完成",
|
||||||
|
"buildCompleteHint": "准备进入下一步骤",
|
||||||
|
"enterEnvSetup": "进入环境搭建",
|
||||||
|
"projectInfo": "项目信息",
|
||||||
|
"projectName": "项目名称",
|
||||||
|
"projectId": "项目ID",
|
||||||
|
"graphId": "图谱ID",
|
||||||
|
"simulationRequirement": "模拟需求",
|
||||||
|
"statusBuildFailed": "构建失败",
|
||||||
|
"statusBuildComplete": "构建完成",
|
||||||
|
"statusGraphBuilding": "图谱构建中",
|
||||||
|
"statusOntologyInProgress": "本体生成中",
|
||||||
|
"statusInitializing": "初始化中",
|
||||||
|
"envSetupComingSoon": "环境搭建功能开发中...",
|
||||||
|
"stepCompleted": "已完成",
|
||||||
|
"stepInProgress": "进行中",
|
||||||
|
"stepWaiting": "等待中",
|
||||||
|
"noFilesError": "没有待上传的文件,请返回首页重新操作",
|
||||||
|
"uploadingFiles": "正在上传文件并分析文档...",
|
||||||
|
"ontologyGenerationFailed": "本体生成失败",
|
||||||
|
"projectInitFailed": "项目初始化失败: {error}",
|
||||||
|
"loadProjectFailed": "加载项目失败",
|
||||||
|
"loadProjectFailedDetail": "加载项目失败: {error}",
|
||||||
|
"processingFailed": "处理失败",
|
||||||
|
"graphBuildStarting": "正在启动图谱构建...",
|
||||||
|
"graphBuildTaskStarted": "图谱构建任务已启动...",
|
||||||
|
"graphBuildStartFailed": "启动图谱构建失败",
|
||||||
|
"graphBuildStartFailedDetail": "启动图谱构建失败: {error}",
|
||||||
|
"graphProcessing": "处理中...",
|
||||||
|
"graphBuildLoadingFull": "构建完成,正在加载图谱...",
|
||||||
|
"graphBuildFailedDetail": "图谱构建失败: {error}",
|
||||||
|
"waitingGraphData": "等待图谱数据...",
|
||||||
|
"fallbackNodeName": "未命名",
|
||||||
|
"realtimeUpdating": "实时更新中..."
|
||||||
|
},
|
||||||
"step1": {
|
"step1": {
|
||||||
"ontologyGeneration": "本体生成",
|
"ontologyGeneration": "本体生成",
|
||||||
"ontologyCompleted": "已完成",
|
"ontologyCompleted": "已完成",
|
||||||
|
|
@ -211,7 +274,8 @@
|
||||||
"forceStopSuccess": "模拟已强制停止",
|
"forceStopSuccess": "模拟已强制停止",
|
||||||
"forceStopFailed": "强制停止失败: {error}",
|
"forceStopFailed": "强制停止失败: {error}",
|
||||||
"startGenerateReportBtn": "开始生成结果报告",
|
"startGenerateReportBtn": "开始生成结果报告",
|
||||||
"generatingReportBtn": "启动中..."
|
"generatingReportBtn": "启动中...",
|
||||||
|
"startFailedFallback": "启动失败"
|
||||||
},
|
},
|
||||||
"step4": {
|
"step4": {
|
||||||
"generatingSection": "正在生成{title}...",
|
"generatingSection": "正在生成{title}...",
|
||||||
|
|
@ -253,7 +317,9 @@
|
||||||
"panelRelatedEdges": "相关关系",
|
"panelRelatedEdges": "相关关系",
|
||||||
"panelRelatedNodes": "相关节点",
|
"panelRelatedNodes": "相关节点",
|
||||||
"world1": "世界1",
|
"world1": "世界1",
|
||||||
"world2": "世界2"
|
"world2": "世界2",
|
||||||
|
"selectionReason": "选择理由",
|
||||||
|
"awaitingStart": "等待开始"
|
||||||
},
|
},
|
||||||
"step5": {
|
"step5": {
|
||||||
"interactiveTools": "交互工具",
|
"interactiveTools": "交互工具",
|
||||||
|
|
@ -288,7 +354,11 @@
|
||||||
"errorOccurred": "抱歉,发生了错误: {error}",
|
"errorOccurred": "抱歉,发生了错误: {error}",
|
||||||
"noResponse": "无响应",
|
"noResponse": "无响应",
|
||||||
"requestFailed": "请求失败",
|
"requestFailed": "请求失败",
|
||||||
"selectAgentFirst": "请先选择一个模拟个体"
|
"selectAgentFirst": "请先选择一个模拟个体",
|
||||||
|
"chatRolePrompter": "提问者",
|
||||||
|
"chatRoleYou": "你",
|
||||||
|
"chatHistoryRoleLine": "{role}:{content}",
|
||||||
|
"chatHistoryPrompt": "以下是我们之前的对话:\n{history}\n\n现在我的新问题是:{message}"
|
||||||
},
|
},
|
||||||
"graph": {
|
"graph": {
|
||||||
"panelTitle": "图谱关系可视化",
|
"panelTitle": "图谱关系可视化",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue