mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 22:16:55 +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 { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
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 { useOpenRouterCatalog, useRefreshOpenRouterModels } from '@/hooks/use-openrouter-models';
|
||||||
import {
|
import {
|
||||||
searchModels,
|
searchModels,
|
||||||
formatPricingPair,
|
formatPricingPair,
|
||||||
formatContextLength,
|
formatContextLength,
|
||||||
|
formatModelAge,
|
||||||
|
getNewestModelsPerProvider,
|
||||||
CATEGORY_LABELS,
|
CATEGORY_LABELS,
|
||||||
} from '@/lib/openrouter-utils';
|
} from '@/lib/openrouter-utils';
|
||||||
import type { CategorizedModel, ModelCategory } from '@/lib/openrouter-types';
|
import type { CategorizedModel, ModelCategory } from '@/lib/openrouter-types';
|
||||||
@@ -46,6 +48,14 @@ export function OpenRouterModelPicker({
|
|||||||
});
|
});
|
||||||
}, [models, search, selectedCategory]);
|
}, [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
|
// Group by category
|
||||||
const groupedModels = useMemo(() => {
|
const groupedModels = useMemo(() => {
|
||||||
const groups: Record<ModelCategory, CategorizedModel[]> = {
|
const groups: Record<ModelCategory, CategorizedModel[]> = {
|
||||||
@@ -159,6 +169,26 @@ export function OpenRouterModelPicker({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 p-2">
|
<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) => {
|
{(Object.keys(CATEGORY_LABELS) as ModelCategory[]).map((category) => {
|
||||||
const categoryModels = groupedModels[category];
|
const categoryModels = groupedModels[category];
|
||||||
if (categoryModels.length === 0) return null;
|
if (categoryModels.length === 0) return null;
|
||||||
@@ -190,10 +220,12 @@ function ModelItem({
|
|||||||
model,
|
model,
|
||||||
isSelected,
|
isSelected,
|
||||||
onClick,
|
onClick,
|
||||||
|
showAge = false,
|
||||||
}: {
|
}: {
|
||||||
model: CategorizedModel;
|
model: CategorizedModel;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
showAge?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -206,6 +238,11 @@ function ModelItem({
|
|||||||
>
|
>
|
||||||
<span className="flex-1 truncate">{model.name}</span>
|
<span className="flex-1 truncate">{model.name}</span>
|
||||||
<span className="text-muted-foreground ml-2 flex items-center gap-2 text-xs">
|
<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 ? (
|
{model.isFree ? (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
Free
|
Free
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface OpenRouterModel {
|
|||||||
top_provider: OpenRouterTopProvider;
|
top_provider: OpenRouterTopProvider;
|
||||||
supported_parameters: string[];
|
supported_parameters: string[];
|
||||||
per_request_limits: Record<string, string> | null;
|
per_request_limits: Record<string, string> | null;
|
||||||
|
created: number; // Unix timestamp when model was added to OpenRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenRouterModelsResponse {
|
export interface OpenRouterModelsResponse {
|
||||||
|
|||||||
@@ -174,3 +174,62 @@ export const CATEGORY_LABELS: Record<ModelCategory, string> = {
|
|||||||
opensource: 'Open Source',
|
opensource: 'Open Source',
|
||||||
other: 'Other',
|
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