mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 04:15:52 +00:00
feat(ui): add streamlined OpenRouter profile editor
- redesign friendly-ui-section with OpenRouter-specific view - add model selection, tier mapping, API key sections - compact ModelItem layout for better space efficiency - add onEnvBulkChange prop for atomic env updates
This commit is contained in:
1 parent
4f4ab43eb3
commit
7788137f1c
4 files changed
+218
-72
No files matched your search
@@ -1,16 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Friendly UI Section
|
* Friendly UI Section
|
||||||
* Left column with environment variables and info tabs
|
* Left column with environment variables and info tabs
|
||||||
* Enhanced with OpenRouter model picker when applicable
|
* Enhanced with OpenRouter-specific streamlined UI when applicable
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
import { EnvEditorSection } from './env-editor-section';
|
import { EnvEditorSection } from './env-editor-section';
|
||||||
import { InfoSection } from './info-section';
|
import { InfoSection } from './info-section';
|
||||||
import { OpenRouterModelPicker } from '@/components/profiles/openrouter-model-picker';
|
import { OpenRouterModelPicker } from '@/components/profiles/openrouter-model-picker';
|
||||||
import { ModelTierMapping, type TierMapping } from '@/components/profiles/model-tier-mapping';
|
import { ModelTierMapping, type TierMapping } from '@/components/profiles/model-tier-mapping';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { MaskedInput } from '@/components/ui/masked-input';
|
||||||
|
import { ChevronRight, Settings2 } from 'lucide-react';
|
||||||
import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './utils';
|
import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { Settings, SettingsResponse } from './types';
|
import type { Settings, SettingsResponse } from './types';
|
||||||
|
|
||||||
interface FriendlyUISectionProps {
|
interface FriendlyUISectionProps {
|
||||||
@@ -46,9 +52,27 @@ export function FriendlyUISection({
|
|||||||
// Memoize currentEnv for consistent reference
|
// Memoize currentEnv for consistent reference
|
||||||
const currentEnv = settingsEnv ?? {};
|
const currentEnv = settingsEnv ?? {};
|
||||||
|
|
||||||
// Handle model selection from OpenRouter picker
|
// Handle model selection from OpenRouter picker - applies to ALL tiers
|
||||||
const handleModelChange = (modelId: string) => {
|
const handleModelChange = (modelId: string) => {
|
||||||
onEnvValueChange('ANTHROPIC_MODEL', modelId);
|
if (onEnvBulkChange) {
|
||||||
|
// Update all 4 model tiers at once
|
||||||
|
const newEnv = {
|
||||||
|
...currentEnv,
|
||||||
|
ANTHROPIC_MODEL: modelId,
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: modelId,
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: modelId,
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelId,
|
||||||
|
};
|
||||||
|
onEnvBulkChange(newEnv);
|
||||||
|
} else {
|
||||||
|
// Fallback: update one by one
|
||||||
|
onEnvValueChange('ANTHROPIC_MODEL', modelId);
|
||||||
|
onEnvValueChange('ANTHROPIC_DEFAULT_OPUS_MODEL', modelId);
|
||||||
|
onEnvValueChange('ANTHROPIC_DEFAULT_SONNET_MODEL', modelId);
|
||||||
|
onEnvValueChange('ANTHROPIC_DEFAULT_HAIKU_MODEL', modelId);
|
||||||
|
}
|
||||||
|
// Show feedback toast
|
||||||
|
toast.success('Applied model to all tiers', { duration: 2000 });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle tier mapping change
|
// Handle tier mapping change
|
||||||
@@ -71,13 +95,30 @@ export function FriendlyUISection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// State for collapsible sections
|
||||||
|
const [showAllEnvVars, setShowAllEnvVars] = useState(false);
|
||||||
|
|
||||||
|
// For OpenRouter: filter out model-related env vars from the main display
|
||||||
|
// These are managed by the model picker and tier mapping
|
||||||
|
const openRouterManagedKeys = new Set([
|
||||||
|
'ANTHROPIC_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||||
|
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Count of hidden env vars for OpenRouter profiles
|
||||||
|
const hiddenEnvVarCount = isOpenRouter
|
||||||
|
? Object.keys(currentEnv).filter((k) => openRouterManagedKeys.has(k)).length
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full w-full min-w-0 flex flex-col">
|
||||||
<Tabs defaultValue="env" className="h-full flex flex-col">
|
<Tabs defaultValue="env" className="h-full w-full min-w-0 flex flex-col">
|
||||||
<div className="px-4 pt-4 shrink-0">
|
<div className="px-4 pt-4 shrink-0">
|
||||||
<TabsList className="w-full">
|
<TabsList className="w-full">
|
||||||
<TabsTrigger value="env" className="flex-1">
|
<TabsTrigger value="env" className="flex-1">
|
||||||
Environment Variables
|
{isOpenRouter ? 'Configuration' : 'Environment Variables'}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="info" className="flex-1">
|
<TabsTrigger value="info" className="flex-1">
|
||||||
Info & Usage
|
Info & Usage
|
||||||
@@ -85,39 +126,114 @@ export function FriendlyUISection({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden flex flex-col">
|
<div className="flex-1 overflow-hidden flex flex-col min-w-0">
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="env"
|
value="env"
|
||||||
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden"
|
className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden min-w-0"
|
||||||
>
|
>
|
||||||
{/* OpenRouter Model Picker Section */}
|
{/* OpenRouter Streamlined View */}
|
||||||
{isOpenRouter && (
|
{isOpenRouter ? (
|
||||||
<div className="px-4 pt-4 pb-2 border-b space-y-3 shrink-0">
|
<div className="flex-1 overflow-hidden">
|
||||||
<div className="space-y-2">
|
<div className="h-full overflow-y-auto overflow-x-hidden p-4 space-y-6">
|
||||||
<label className="text-xs font-medium text-muted-foreground">
|
{/* Model Selection - Primary Focus */}
|
||||||
Model (OpenRouter)
|
<div className="space-y-3">
|
||||||
</label>
|
<Label className="text-sm font-medium">Model Selection</Label>
|
||||||
<OpenRouterModelPicker
|
<OpenRouterModelPicker
|
||||||
value={currentEnv.ANTHROPIC_MODEL}
|
value={currentEnv.ANTHROPIC_MODEL}
|
||||||
onChange={handleModelChange}
|
onChange={handleModelChange}
|
||||||
placeholder="Search OpenRouter models..."
|
placeholder="Search OpenRouter models..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ModelTierMapping
|
|
||||||
selectedModel={currentEnv.ANTHROPIC_MODEL}
|
|
||||||
value={tierMapping}
|
|
||||||
onChange={handleTierMappingChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EnvEditorSection
|
{/* Model Tier Mapping - Collapsible */}
|
||||||
currentSettings={currentSettings}
|
<ModelTierMapping
|
||||||
newEnvKey={newEnvKey}
|
selectedModel={currentEnv.ANTHROPIC_MODEL}
|
||||||
onNewEnvKeyChange={onNewEnvKeyChange}
|
value={tierMapping}
|
||||||
onEnvValueChange={onEnvValueChange}
|
onChange={handleTierMappingChange}
|
||||||
onAddEnvVar={onAddEnvVar}
|
/>
|
||||||
/>
|
|
||||||
|
{/* API Key - Simplified */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">API Key</Label>
|
||||||
|
<MaskedInput
|
||||||
|
value={currentEnv.ANTHROPIC_AUTH_TOKEN || ''}
|
||||||
|
onChange={(e) => onEnvValueChange('ANTHROPIC_AUTH_TOKEN', e.target.value)}
|
||||||
|
placeholder="sk-or-v1-..."
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get your API key from{' '}
|
||||||
|
<a
|
||||||
|
href="https://openrouter.ai/keys"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
openrouter.ai/keys
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced: All Environment Variables */}
|
||||||
|
<Collapsible open={showAllEnvVars} onOpenChange={setShowAllEnvVars}>
|
||||||
|
<CollapsibleTrigger className="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors group">
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 transition-transform',
|
||||||
|
showAllEnvVars && 'rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Settings2 className="h-4 w-4" />
|
||||||
|
<span>All Environment Variables</span>
|
||||||
|
<span className="text-xs font-normal opacity-70">
|
||||||
|
({Object.keys(currentEnv).length} vars
|
||||||
|
{hiddenEnvVarCount > 0 && `, ${hiddenEnvVarCount} managed by picker`})
|
||||||
|
</span>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="pt-4">
|
||||||
|
<div className="space-y-3 border rounded-lg p-3 bg-muted/30">
|
||||||
|
{Object.entries(currentEnv).map(([key, value]) => (
|
||||||
|
<div key={key} className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground flex items-center gap-2">
|
||||||
|
{key}
|
||||||
|
{openRouterManagedKeys.has(key) && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 bg-primary/10 text-primary rounded">
|
||||||
|
managed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
{key === 'ANTHROPIC_AUTH_TOKEN' ? (
|
||||||
|
<MaskedInput
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onEnvValueChange(key, e.target.value)}
|
||||||
|
className="font-mono text-xs h-8"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onEnvValueChange(key, e.target.value)}
|
||||||
|
className="w-full font-mono text-xs h-8 px-2 rounded border bg-background"
|
||||||
|
readOnly={openRouterManagedKeys.has(key)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Standard Env Editor for non-OpenRouter profiles */
|
||||||
|
<EnvEditorSection
|
||||||
|
currentSettings={currentSettings}
|
||||||
|
newEnvKey={newEnvKey}
|
||||||
|
onNewEnvKeyChange={onNewEnvKeyChange}
|
||||||
|
onEnvValueChange={onEnvValueChange}
|
||||||
|
onAddEnvVar={onAddEnvVar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent
|
<TabsContent
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function ProfileEditor({ profileName, onDelete }: ProfileEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden">
|
<div className="flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden">
|
||||||
<div className="flex flex-col overflow-hidden bg-muted/5">
|
<div className="flex flex-col overflow-hidden bg-muted/5 min-w-0">
|
||||||
<FriendlyUISection
|
<FriendlyUISection
|
||||||
profileName={profileName}
|
profileName={profileName}
|
||||||
data={data}
|
data={data}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function OpenRouterModelPicker({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('space-y-2', className)}>
|
<div className={cn('space-y-2 w-full min-w-0 overflow-hidden', className)}>
|
||||||
{/* Search Header */}
|
{/* Search Header */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
@@ -155,7 +155,7 @@ export function OpenRouterModelPicker({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Model List */}
|
{/* Model List */}
|
||||||
<ScrollArea className="h-64 rounded-md border">
|
<ScrollArea className="h-72 w-full rounded-md border">
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<div className="text-destructive p-4 text-center">
|
<div className="text-destructive p-4 text-center">
|
||||||
Failed to load models.{' '}
|
Failed to load models.{' '}
|
||||||
@@ -168,23 +168,25 @@ export function OpenRouterModelPicker({
|
|||||||
No models found matching "{search}"
|
No models found matching "{search}"
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 p-2">
|
<div className="space-y-6 p-3">
|
||||||
{/* Newest Models Section (shown when no search) */}
|
{/* Newest Models Section (shown when no search) */}
|
||||||
{showPresets && newestModels.length > 0 && (
|
{showPresets && newestModels.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="text-muted-foreground bg-background sticky top-0 mb-2 flex items-center gap-1.5 py-1.5 text-xs font-semibold border-b pb-2">
|
||||||
<Sparkles className="h-3 w-3 text-accent" />
|
<Sparkles className="h-3 w-3 text-accent" />
|
||||||
<span>Newest Models</span>
|
<span>Newest Models</span>
|
||||||
</div>
|
</div>
|
||||||
{newestModels.map((model) => (
|
<div className="space-y-1">
|
||||||
<ModelItem
|
{newestModels.map((model) => (
|
||||||
key={model.id}
|
<ModelItem
|
||||||
model={model}
|
key={model.id}
|
||||||
isSelected={model.id === value}
|
model={model}
|
||||||
onClick={() => onChange(model.id)}
|
isSelected={model.id === value}
|
||||||
showAge
|
onClick={() => onChange(model.id)}
|
||||||
/>
|
showAge
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -195,17 +197,19 @@ export function OpenRouterModelPicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category}>
|
<div key={category}>
|
||||||
<div className="text-muted-foreground bg-background sticky top-0 mb-1 py-1 text-xs font-semibold">
|
<div className="text-muted-foreground bg-background sticky top-0 mb-2 py-1.5 text-xs font-semibold border-b pb-2">
|
||||||
{CATEGORY_LABELS[category]}
|
{CATEGORY_LABELS[category]}
|
||||||
</div>
|
</div>
|
||||||
{categoryModels.map((model) => (
|
<div className="space-y-1">
|
||||||
<ModelItem
|
{categoryModels.map((model) => (
|
||||||
key={model.id}
|
<ModelItem
|
||||||
model={model}
|
key={model.id}
|
||||||
isSelected={model.id === value}
|
model={model}
|
||||||
onClick={() => onChange(model.id)}
|
isSelected={model.id === value}
|
||||||
/>
|
onClick={() => onChange(model.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -232,39 +236,49 @@ function ModelItem({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm transition-colors',
|
'group flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors',
|
||||||
'hover:bg-accent hover:text-accent-foreground',
|
'hover:bg-accent hover:text-accent-foreground',
|
||||||
isSelected && 'bg-accent text-accent-foreground'
|
isSelected && 'bg-accent text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="flex-1 truncate">{model.name}</span>
|
<span className="flex-1 min-w-0 truncate font-medium">{model.name}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-2 flex items-center gap-2 text-xs',
|
'flex shrink-0 items-center gap-1 text-xs whitespace-nowrap',
|
||||||
isSelected ? 'text-accent-foreground/80' : 'text-muted-foreground'
|
isSelected
|
||||||
|
? 'text-accent-foreground/80'
|
||||||
|
: 'text-muted-foreground group-hover:text-accent-foreground/80'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{showAge && model.created && (
|
{showAge && model.created && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px]',
|
'text-[10px] px-1',
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-accent-foreground/30 text-accent-foreground/80'
|
? 'border-accent-foreground/30 text-accent-foreground/80'
|
||||||
: 'text-accent border-accent/30'
|
: 'text-accent border-accent/30 group-hover:text-accent-foreground/80 group-hover:border-accent-foreground/30'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatModelAge(model.created)}
|
{formatModelAge(model.created)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{model.isFree ? (
|
{model.isFree ? (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={cn(
|
||||||
|
'text-[10px] px-1',
|
||||||
|
isSelected
|
||||||
|
? 'bg-accent-foreground/20 text-accent-foreground'
|
||||||
|
: 'group-hover:bg-accent-foreground/20 group-hover:text-accent-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
Free
|
Free
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<span>{formatPricingPair(model.pricing)}</span>
|
<span className="tabular-nums">{formatPricingPair(model.pricing)}</span>
|
||||||
)}
|
)}
|
||||||
<span>{formatContextLength(model.context_length)}</span>
|
<span className="tabular-nums">{formatContextLength(model.context_length)}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr
|
|||||||
setValue('sonnetModel', model.id);
|
setValue('sonnetModel', model.id);
|
||||||
setValue('haikuModel', model.id);
|
setValue('haikuModel', model.id);
|
||||||
setModelSearch(model.name);
|
setModelSearch(model.name);
|
||||||
|
// Show feedback that model was applied to all tiers
|
||||||
|
toast.success(`Applied "${model.name}" to all model tiers`, {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for common URL mistakes - only for truly custom URLs
|
// Check for common URL mistakes - only for truly custom URLs
|
||||||
@@ -401,6 +405,12 @@ export function ProfileCreateDialog({ open, onOpenChange, onSuccess }: ProfileCr
|
|||||||
value={modelSearch}
|
value={modelSearch}
|
||||||
onChange={(e) => setModelSearch(e.target.value)}
|
onChange={(e) => setModelSearch(e.target.value)}
|
||||||
placeholder="Type to search (e.g., opus, sonnet, gpt-4o)..."
|
placeholder="Type to search (e.g., opus, sonnet, gpt-4o)..."
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && filteredModels.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleModelSelect(filteredModels[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="border rounded-md max-h-48 overflow-y-auto">
|
<div className="border rounded-md max-h-48 overflow-y-auto">
|
||||||
{filteredModels.length === 0 ? (
|
{filteredModels.length === 0 ? (
|
||||||
@@ -581,17 +591,23 @@ function ModelSearchItem({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent"
|
className="group flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<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 group-hover:text-accent-foreground/80 ml-2 flex items-center gap-2 text-xs">
|
||||||
{showAge && model.created && (
|
{showAge && model.created && (
|
||||||
<Badge variant="outline" className="text-[10px] text-accent">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] text-accent group-hover:text-accent-foreground/80 group-hover:border-accent-foreground/30"
|
||||||
|
>
|
||||||
{formatModelAge(model.created)}
|
{formatModelAge(model.created)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{model.isFree ? (
|
{model.isFree ? (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs group-hover:bg-accent-foreground/20 group-hover:text-accent-foreground"
|
||||||
|
>
|
||||||
Free
|
Free
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in new issue
Block a user