diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 9f1595db..c05a8545 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1842,6 +1842,10 @@ export default { promptHelp: 'Supports {input_text} and {title} placeholders', saveBtn: 'Save', cancelBtn: 'Cancel', + compareBtn: 'Compare selected', + compareHint: 'Pick 1 more run to compare', + compareCheckTip: 'Tick 2 completed runs to compare side-by-side', + compareTitle: 'Compare runs', aggregateBtn: 'Aggregate all runs', aggregating: 'Aggregating…', aggregateDone: 'Aggregate page produced', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 355aa3bf..ac60df27 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1854,6 +1854,10 @@ export default { promptHelp: '可使用 {input_text} 和 {title} 占位符', saveBtn: '保存', cancelBtn: '取消', + compareBtn: '对比所选', + compareHint: '再选 1 条即可对比', + compareCheckTip: '勾选 2 条已完成运行可并排对比', + compareTitle: '运行结果对比', aggregateBtn: '聚合所有运行', aggregating: '聚合中…', aggregateDone: '聚合页已生成', diff --git a/mateclaw-ui/src/views/Wiki/components/TransformationsPanel.vue b/mateclaw-ui/src/views/Wiki/components/TransformationsPanel.vue index c6073d15..62b125eb 100644 --- a/mateclaw-ui/src/views/Wiki/components/TransformationsPanel.vue +++ b/mateclaw-ui/src/views/Wiki/components/TransformationsPanel.vue @@ -87,9 +87,29 @@
-
{{ t('wiki.transformations.runs') }}
+
+
{{ t('wiki.transformations.runs') }}
+ + + {{ t('wiki.transformations.compareHint') }} + +
+ {{ run.status }} {{ rawTitleFor(run.rawId) }} @@ -260,6 +280,39 @@
+ + + @@ -325,6 +378,10 @@ const savingRunId = ref(null) const cancellingRunId = ref(null) const rerunningRunId = ref(null) const aggregatingTemplateId = ref(null) +// Map of templateId → up-to-2 run IDs the user has ticked for side-by-side compare. +const selectedForCompare = ref>({}) +const compareOpen = ref(false) +const compareRuns = ref<(WikiTransformationRun | undefined)[]>([]) interface ModelOption { id: number; name: string; provider: string; modelName: string } const availableModels = ref([]) @@ -557,6 +614,43 @@ async function onSaveRunAsPage(tpl: WikiTransformation, run: WikiTransformationR } } +function isSelectedForCompare(tplId: number, runId: number): boolean { + return (selectedForCompare.value[tplId] || []).includes(runId) +} + +function toggleCompareSelect(tplId: number, runId: number) { + const existing = selectedForCompare.value[tplId] || [] + if (existing.includes(runId)) { + selectedForCompare.value[tplId] = existing.filter((id) => id !== runId) + return + } + if (existing.length >= 2) { + // Sliding window: drop the oldest selection to make room for the new pick. + selectedForCompare.value[tplId] = [existing[1], runId] + } else { + selectedForCompare.value[tplId] = [...existing, runId] + } +} + +function openCompare(tpl: WikiTransformation) { + const ids = selectedForCompare.value[tpl.id] || [] + if (ids.length !== 2) return + const runs = runsByTemplate.value[tpl.id] || [] + // Order chronologically (older first) so left = before, right = after. + const picked = ids + .map((id) => runs.find((r) => r.id === id)) + .filter((r): r is WikiTransformationRun => !!r) + .sort((a, b) => new Date(a.createTime).getTime() - new Date(b.createTime).getTime()) + if (picked.length !== 2) return + compareRuns.value = picked + compareOpen.value = true +} + +function closeCompare() { + compareOpen.value = false + compareRuns.value = [] +} + async function onAggregate(tpl: WikiTransformation) { if (!store.currentKB) return aggregatingTemplateId.value = tpl.id @@ -736,7 +830,11 @@ onMounted(async () => { } .runs-list { display: flex; flex-direction: column; gap: 6px; padding-top: 8px; border-top: 1px dashed var(--mc-border-light); } -.runs-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--mc-text-tertiary); } +.runs-head-row { display: flex; align-items: center; gap: 10px; } +.runs-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--mc-text-tertiary); flex: 1; } +.btn-compare { font-size: 12px; padding: 4px 12px; } +.compare-hint { font-size: 11px; color: var(--mc-text-tertiary); } +.compare-check { margin-right: 6px; accent-color: var(--mc-primary); cursor: pointer; } .run-item summary { cursor: pointer; display: flex; @@ -815,6 +913,40 @@ onMounted(async () => { overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); } +.modal--compare { width: min(1200px, 96vw); } +.compare-body { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 14px 16px; overflow-y: auto; } +.compare-col { display: flex; flex-direction: column; gap: 8px; min-width: 0; } +.compare-col-head { + background: var(--mc-bg-muted); + border: 1px solid var(--mc-border-light); + border-radius: 10px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 4px; +} +.compare-col-title { font-size: 13px; font-weight: 600; color: var(--mc-text-primary); } +.compare-col-meta { font-size: 11px; color: var(--mc-text-secondary); display: flex; gap: 6px; align-items: center; flex-wrap: wrap; } +.compare-col-source { font-size: 11px; color: var(--mc-text-tertiary); font-family: var(--mc-font-mono, ui-monospace, Menlo, monospace); } +.compare-col-output { + flex: 1; + margin: 0; + padding: 12px 14px; + background: var(--mc-bg); + border: 1px solid var(--mc-border-light); + border-radius: 10px; + font-family: var(--mc-font-mono, ui-monospace, Menlo, monospace); + font-size: 12px; + line-height: 1.55; + color: var(--mc-text-primary); + white-space: pre-wrap; + word-break: break-word; + overflow: auto; + min-height: 360px; +} +@media (max-width: 900px) { + .compare-body { grid-template-columns: 1fr; } +} .modal-head { display: flex; justify-content: space-between;