Skip to content

Commit 767903e

Browse files
authored
Merge pull request #144 from aicodingstack/agent/model-comparison-flow
feat(models): unify comparison flow
2 parents ce46992 + b339b6b commit 767903e

13 files changed

Lines changed: 210 additions & 850 deletions

File tree

docs/PROJECT-REVIEW-2026-07-18.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ Baseline captured on 2026-07-18:
7979
## P1 — Product Experience
8080

8181
- [x] Stop statically generating every possible two-model comparison pair.
82-
- [ ] Unify `/models/comparison` and `/models/compare/...` into one understandable comparison
82+
- [x] Unify `/models/comparison` and `/models/compare/...` into one understandable comparison
8383
journey.
84-
- [ ] Add “compare” actions to cards and detail pages with persistent selected items.
84+
- [x] Add “compare” actions to cards and detail pages with persistent selected items.
8585
- [x] Search localized names/descriptions, capabilities, modalities, platforms, vendors, and types.
8686
- [ ] Add explicit use-case metadata and include it in search when the schema supports it.
8787
- [ ] Add guided selection filters: interface, budget, model freedom, privacy/local execution,
@@ -167,3 +167,7 @@ evidence; completed items are reflected in the checklists and implementation log
167167
providers and representative GPT-5.2, Claude Haiku 4.5, and Gemini 3 Flash model records.
168168
- Corrected GPT-5.2 from `latest` to `maintained` after its official model page identified it as a
169169
previous frontier model that remains available.
170+
- Made `/models/compare` the canonical model comparison journey, retained the old all-model URL as
171+
a permanent redirect, and removed its duplicate comparison implementation.
172+
- Added a persistent two-model selection shared by model cards, detail-page comparison actions,
173+
canonical pair URLs, and the comparison selector; adding a third model replaces the oldest pick.

src/app/[locale]/models/compare/[models]/page.client.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import { ExternalLink } from 'lucide-react'
44
import { useTranslations } from 'next-intl'
5-
import { Fragment, useEffect, useState } from 'react'
5+
import { Fragment, useEffect, useRef } from 'react'
6+
import { useModelComparison } from '@/components/controls/useModelComparison'
67
import type { Locale } from '@/i18n/config'
78
import { Link, useRouter } from '@/i18n/navigation'
89
import { providersData } from '@/lib/generated'
10+
import { buildModelComparisonPath } from '@/lib/model-comparison'
911
import type {
1012
ManifestBenchmarks,
1113
ManifestModel,
@@ -180,10 +182,17 @@ export default function ComparePageClient({
180182
const router = useRouter()
181183
const tPage = useTranslations('pages.modelCompare')
182184
const tShared = useTranslations('shared')
183-
const [selectedModel1, setSelectedModel1] = useState<string>(initialModels[0]?.id ?? '')
184-
const [selectedModel2, setSelectedModel2] = useState<string>(initialModels[1]?.id ?? '')
185-
const [prevModel1, setPrevModel1] = useState<string>(initialModels[0]?.id ?? '')
186-
const [prevModel2, setPrevModel2] = useState<string>(initialModels[1]?.id ?? '')
185+
const initialModelIds = initialModels.map(model => model.id)
186+
const { selectedIds, isHydrated, setSelection } = useModelComparison(initialModelIds)
187+
const selectedModel1 = selectedIds[0] ?? ''
188+
const selectedModel2 = selectedIds[1] ?? ''
189+
const lastComparisonPath = useRef(buildModelComparisonPath(initialModelIds))
190+
191+
const updateModelSlot = (slotIndex: number, value: string) => {
192+
const nextSelection = [selectedModel1, selectedModel2]
193+
nextSelection[slotIndex] = value
194+
setSelection(nextSelection.filter(Boolean))
195+
}
187196

188197
const findProviderId = (vendor: string): string | null => {
189198
const normalizedVendor = vendor.toLocaleLowerCase()
@@ -471,17 +480,14 @@ export default function ComparePageClient({
471480
)
472481
}
473482

474-
// Update URL when both models are selected and at least one has changed
483+
// Keep the shareable URL aligned with the persisted selection.
475484
useEffect(() => {
476-
if (selectedModel1 && selectedModel2 && selectedModel1 !== selectedModel2) {
477-
if (selectedModel1 !== prevModel1 || selectedModel2 !== prevModel2) {
478-
const url = `/models/compare/${selectedModel1}-vs-${selectedModel2}`
479-
router.push(url)
480-
setPrevModel1(selectedModel1)
481-
setPrevModel2(selectedModel2)
482-
}
483-
}
484-
}, [selectedModel1, selectedModel2, prevModel1, prevModel2, router])
485+
if (!isHydrated) return
486+
const nextPath = buildModelComparisonPath(selectedIds)
487+
if (nextPath === lastComparisonPath.current) return
488+
lastComparisonPath.current = nextPath
489+
router.replace(nextPath)
490+
}, [isHydrated, router, selectedIds])
485491

486492
const model1 = allModels.find(m => m.id === selectedModel1)
487493
const model2 = allModels.find(m => m.id === selectedModel2)
@@ -498,7 +504,7 @@ export default function ComparePageClient({
498504
<div className="flex flex-col gap-2 items-center">
499505
<ModelSelect
500506
value={selectedModel1}
501-
onChange={setSelectedModel1}
507+
onChange={value => updateModelSlot(0, value)}
502508
availableModels={getAvailableModelsForSlot(0)}
503509
/>
504510
</div>
@@ -507,7 +513,7 @@ export default function ComparePageClient({
507513
<div className="flex flex-col gap-2 items-center">
508514
<ModelSelect
509515
value={selectedModel2}
510-
onChange={setSelectedModel2}
516+
onChange={value => updateModelSlot(1, value)}
511517
availableModels={getAvailableModelsForSlot(1)}
512518
/>
513519
</div>

0 commit comments

Comments
 (0)