mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
feat(ui): add dynamic newest models detection for OpenRouter
- add created timestamp field to OpenRouterModel type - add getNewestModelsPerProvider() for dynamic newest models - add formatModelAge() for relative time display (e.g., "2d ago") - show newest models section in picker when no search query - sort search results by created date (newest first)
This commit is contained in:
@@ -9,12 +9,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Search, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { Search, RefreshCw, Loader2, Sparkles } from 'lucide-react';
|
||||
import { useOpenRouterCatalog, useRefreshOpenRouterModels } from '@/hooks/use-openrouter-models';
|
||||
import {
|
||||
searchModels,
|
||||
formatPricingPair,
|
||||
formatContextLength,
|
||||
formatModelAge,
|
||||
getNewestModelsPerProvider,
|
||||
CATEGORY_LABELS,
|
||||
} from '@/lib/openrouter-utils';
|
||||
import type { CategorizedModel, ModelCategory } from '@/lib/openrouter-types';
|
||||
@@ -46,6 +48,14 @@ export function OpenRouterModelPicker({
|
||||
});
|
||||
}, [models, search, selectedCategory]);
|
||||
|
||||
// Get newest models for presets (shown when no search)
|
||||
const newestModels = useMemo(() => {
|
||||
return getNewestModelsPerProvider(models, 2);
|
||||
}, [models]);
|
||||
|
||||
// Determine if we should show presets (no search query and no category filter)
|
||||
const showPresets = !search.trim() && !selectedCategory;
|
||||
|
||||
// Group by category
|
||||
const groupedModels = useMemo(() => {
|
||||
const groups: Record<ModelCategory, CategorizedModel[]> = {
|
||||
@@ -159,6 +169,26 @@ export function OpenRouterModelPicker({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 p-2">
|
||||
{/* Newest Models Section (shown when no search) */}
|
||||
{showPresets && newestModels.length > 0 && (
|
||||
<div>
|
||||
<div className="text-muted-foreground bg-background sticky top-0 mb-1 flex items-center gap-1.5 py-1 text-xs font-semibold">
|
||||
<Sparkles className="h-3 w-3 text-orange-500" />
|
||||
<span>Newest Models</span>
|
||||
</div>
|
||||
{newestModels.map((model) => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
isSelected={model.id === value}
|
||||
onClick={() => onChange(model.id)}
|
||||
showAge
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category Groups */}
|
||||
{(Object.keys(CATEGORY_LABELS) as ModelCategory[]).map((category) => {
|
||||
const categoryModels = groupedModels[category];
|
||||
if (categoryModels.length === 0) return null;
|
||||
@@ -190,10 +220,12 @@ function ModelItem({
|
||||
model,
|
||||
isSelected,
|
||||
onClick,
|
||||
showAge = false,
|
||||
}: {
|
||||
model: CategorizedModel;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
showAge?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
@@ -206,6 +238,11 @@ function ModelItem({
|
||||
>
|
||||
<span className="flex-1 truncate">{model.name}</span>
|
||||
<span className="text-muted-foreground ml-2 flex items-center gap-2 text-xs">
|
||||
{showAge && model.created && (
|
||||
<Badge variant="outline" className="text-[10px] text-orange-600">
|
||||
{formatModelAge(model.created)}
|
||||
</Badge>
|
||||
)}
|
||||
{model.isFree ? (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Free
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface OpenRouterModel {
|
||||
top_provider: OpenRouterTopProvider;
|
||||
supported_parameters: string[];
|
||||
per_request_limits: Record<string, string> | null;
|
||||
created: number; // Unix timestamp when model was added to OpenRouter
|
||||
}
|
||||
|
||||
export interface OpenRouterModelsResponse {
|
||||
|
||||
@@ -174,3 +174,62 @@ export const CATEGORY_LABELS: Record<ModelCategory, string> = {
|
||||
opensource: 'Open Source',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
/** Provider prefixes for detecting newest models */
|
||||
const PROVIDER_PREFIXES: Record<ModelCategory, string[]> = {
|
||||
anthropic: ['anthropic/'],
|
||||
openai: ['openai/'],
|
||||
google: ['google/'],
|
||||
meta: ['meta-llama/', 'meta/'],
|
||||
mistral: ['mistralai/'],
|
||||
opensource: ['deepseek/', 'qwen/', 'cohere/'],
|
||||
other: [],
|
||||
};
|
||||
|
||||
/** Get the newest models per provider (sorted by created timestamp) */
|
||||
export function getNewestModelsPerProvider(
|
||||
allModels: CategorizedModel[],
|
||||
modelsPerProvider: number = 2
|
||||
): CategorizedModel[] {
|
||||
const result: CategorizedModel[] = [];
|
||||
const categories: ModelCategory[] = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'meta',
|
||||
'mistral',
|
||||
'opensource',
|
||||
];
|
||||
|
||||
for (const category of categories) {
|
||||
const prefixes = PROVIDER_PREFIXES[category];
|
||||
if (prefixes.length === 0) continue;
|
||||
|
||||
// Get models for this provider
|
||||
const providerModels = allModels.filter((m) =>
|
||||
prefixes.some((prefix) => m.id.toLowerCase().startsWith(prefix))
|
||||
);
|
||||
|
||||
// Sort by created timestamp (newest first)
|
||||
const sorted = [...providerModels].sort((a, b) => (b.created ?? 0) - (a.created ?? 0));
|
||||
|
||||
// Take top N
|
||||
result.push(...sorted.slice(0, modelsPerProvider));
|
||||
}
|
||||
|
||||
// Sort final result by created (newest first)
|
||||
return result.sort((a, b) => (b.created ?? 0) - (a.created ?? 0));
|
||||
}
|
||||
|
||||
/** Format relative time for model creation date */
|
||||
export function formatModelAge(created: number): string {
|
||||
const now = Date.now() / 1000; // Convert to seconds
|
||||
const diff = now - created;
|
||||
|
||||
if (diff < 86400) return 'Today';
|
||||
if (diff < 172800) return 'Yesterday';
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
||||
if (diff < 2592000) return `${Math.floor(diff / 604800)}w ago`;
|
||||
if (diff < 31536000) return `${Math.floor(diff / 2592000)}mo ago`;
|
||||
return `${Math.floor(diff / 31536000)}y ago`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user