mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
feat(wiki): side-by-side compare modal for two transformation runs
This commit is contained in:
parent
465a727be2
commit
8dde62e689
@ -1842,6 +1842,10 @@ export default {
|
|||||||
promptHelp: 'Supports {input_text} and {title} placeholders',
|
promptHelp: 'Supports {input_text} and {title} placeholders',
|
||||||
saveBtn: 'Save',
|
saveBtn: 'Save',
|
||||||
cancelBtn: 'Cancel',
|
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',
|
aggregateBtn: 'Aggregate all runs',
|
||||||
aggregating: 'Aggregating…',
|
aggregating: 'Aggregating…',
|
||||||
aggregateDone: 'Aggregate page produced',
|
aggregateDone: 'Aggregate page produced',
|
||||||
|
|||||||
@ -1854,6 +1854,10 @@ export default {
|
|||||||
promptHelp: '可使用 {input_text} 和 {title} 占位符',
|
promptHelp: '可使用 {input_text} 和 {title} 占位符',
|
||||||
saveBtn: '保存',
|
saveBtn: '保存',
|
||||||
cancelBtn: '取消',
|
cancelBtn: '取消',
|
||||||
|
compareBtn: '对比所选',
|
||||||
|
compareHint: '再选 1 条即可对比',
|
||||||
|
compareCheckTip: '勾选 2 条已完成运行可并排对比',
|
||||||
|
compareTitle: '运行结果对比',
|
||||||
aggregateBtn: '聚合所有运行',
|
aggregateBtn: '聚合所有运行',
|
||||||
aggregating: '聚合中…',
|
aggregating: '聚合中…',
|
||||||
aggregateDone: '聚合页已生成',
|
aggregateDone: '聚合页已生成',
|
||||||
|
|||||||
@ -87,9 +87,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="runsByTemplate[tpl.id]?.length" class="runs-list">
|
<div v-if="runsByTemplate[tpl.id]?.length" class="runs-list">
|
||||||
<div class="runs-title">{{ t('wiki.transformations.runs') }}</div>
|
<div class="runs-head-row">
|
||||||
|
<div class="runs-title">{{ t('wiki.transformations.runs') }}</div>
|
||||||
|
<button
|
||||||
|
v-if="selectedForCompare[tpl.id]?.length === 2"
|
||||||
|
class="btn-secondary btn-compare"
|
||||||
|
@click="openCompare(tpl)"
|
||||||
|
>
|
||||||
|
{{ t('wiki.transformations.compareBtn') }} ({{ selectedForCompare[tpl.id].length }})
|
||||||
|
</button>
|
||||||
|
<span v-else-if="selectedForCompare[tpl.id]?.length === 1" class="compare-hint">
|
||||||
|
{{ t('wiki.transformations.compareHint') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<details v-for="run in runsByTemplate[tpl.id]" :key="run.id" class="run-item">
|
<details v-for="run in runsByTemplate[tpl.id]" :key="run.id" class="run-item">
|
||||||
<summary>
|
<summary>
|
||||||
|
<input
|
||||||
|
v-if="run.status === 'completed'"
|
||||||
|
type="checkbox"
|
||||||
|
class="compare-check"
|
||||||
|
:checked="isSelectedForCompare(tpl.id, run.id)"
|
||||||
|
@click.stop="toggleCompareSelect(tpl.id, run.id)"
|
||||||
|
:title="t('wiki.transformations.compareCheckTip')"
|
||||||
|
/>
|
||||||
<span class="run-status" :class="`run-status--${run.status}`">{{ run.status }}</span>
|
<span class="run-status" :class="`run-status--${run.status}`">{{ run.status }}</span>
|
||||||
<span class="run-meta">
|
<span class="run-meta">
|
||||||
{{ rawTitleFor(run.rawId) }}
|
{{ rawTitleFor(run.rawId) }}
|
||||||
@ -260,6 +280,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Compare modal: shows two completed runs side-by-side. -->
|
||||||
|
<div v-if="compareOpen" class="modal-overlay" @click.self="closeCompare">
|
||||||
|
<div class="modal modal--compare">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>{{ t('wiki.transformations.compareTitle') }}</h3>
|
||||||
|
<button class="modal-close" @click="closeCompare">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body compare-body">
|
||||||
|
<article v-for="(run, idx) in compareRuns" :key="run?.id ?? idx" class="compare-col">
|
||||||
|
<header class="compare-col-head">
|
||||||
|
<div class="compare-col-title">Run #{{ run?.id }}</div>
|
||||||
|
<div class="compare-col-meta">
|
||||||
|
<span>{{ formatTimestamp(run?.completedAt || run?.startedAt || run?.createTime || null) }}</span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span>{{ formatDuration(run?.durationMs ?? null) }}</span>
|
||||||
|
<span v-if="run?.totalTokens" class="dot"></span>
|
||||||
|
<span v-if="run?.totalTokens">
|
||||||
|
{{ formatTokens(run.inputTokens) }}↑ / {{ formatTokens(run.outputTokens) }}↓
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="compare-col-source">
|
||||||
|
{{ run?.inputKind === 'page' ? 'page' : 'raw' }} · {{ run?.rawId != null ? rawTitleFor(run.rawId) : ('page#' + run?.pageId) }}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<pre class="compare-col-output">{{ run?.output ?? '' }}</pre>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-secondary" @click="closeCompare">{{ t('wiki.transformations.cancelBtn') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -325,6 +378,10 @@ const savingRunId = ref<number | null>(null)
|
|||||||
const cancellingRunId = ref<number | null>(null)
|
const cancellingRunId = ref<number | null>(null)
|
||||||
const rerunningRunId = ref<number | null>(null)
|
const rerunningRunId = ref<number | null>(null)
|
||||||
const aggregatingTemplateId = ref<number | null>(null)
|
const aggregatingTemplateId = ref<number | null>(null)
|
||||||
|
// Map of templateId → up-to-2 run IDs the user has ticked for side-by-side compare.
|
||||||
|
const selectedForCompare = ref<Record<number, number[]>>({})
|
||||||
|
const compareOpen = ref(false)
|
||||||
|
const compareRuns = ref<(WikiTransformationRun | undefined)[]>([])
|
||||||
interface ModelOption { id: number; name: string; provider: string; modelName: string }
|
interface ModelOption { id: number; name: string; provider: string; modelName: string }
|
||||||
const availableModels = ref<ModelOption[]>([])
|
const availableModels = ref<ModelOption[]>([])
|
||||||
|
|
||||||
@ -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) {
|
async function onAggregate(tpl: WikiTransformation) {
|
||||||
if (!store.currentKB) return
|
if (!store.currentKB) return
|
||||||
aggregatingTemplateId.value = tpl.id
|
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-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 {
|
.run-item summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -815,6 +913,40 @@ onMounted(async () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
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 {
|
.modal-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user