fix(ui): move chat controls to header, add new session, fix settings layout

Chat page:
- Move session selector, refresh, thinking toggle to the Header bar
  (same level as "Chat" title) instead of inside the chat content area
- Add "New Session" button (+ icon) to create fresh chat sessions
- Remove duplicate toolbar from chat body

Settings page:
- Remove max-w-2xl constraint so cards fill available width
- Redesign provider cards: compact layout with key + actions in one row
- Shorten API key display (sk-...df67 format instead of full masked key)
- Move edit/delete/star buttons inside the key row background area
- Remove duplicate "AI Providers" heading (already in card header)
This commit is contained in:
Haze
2026-02-06 05:11:39 +08:00
Unverified
parent ecb36f0ed8
commit e9ad15bf6f
2 changed files with 75 additions and 69 deletions

View File

@@ -101,15 +101,9 @@ export function ProvidersSettings() {
}; };
return ( return (
<div className="space-y-6"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex justify-end">
<div> <Button size="sm" onClick={() => setShowAddDialog(true)}>
<h3 className="text-lg font-medium">AI Providers</h3>
<p className="text-sm text-muted-foreground">
Configure your AI model providers and API keys
</p>
</div>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Add Provider Add Provider
</Button> </Button>
@@ -180,6 +174,21 @@ interface ProviderCardProps {
onValidateKey: (key: string) => Promise<{ valid: boolean; error?: string }>; onValidateKey: (key: string) => Promise<{ valid: boolean; error?: string }>;
} }
/**
* Shorten a masked key to a more readable format.
* e.g. "sk-or-v1-a20a****df67" -> "sk-...df67"
*/
function shortenKeyDisplay(masked: string | null): string {
if (!masked) return 'No key';
// Show first 4 chars + last 4 chars
if (masked.length > 12) {
const prefix = masked.substring(0, 4);
const suffix = masked.substring(masked.length - 4);
return `${prefix}...${suffix}`;
}
return masked;
}
function ProviderCard({ function ProviderCard({
provider, provider,
isDefault, isDefault,
@@ -225,33 +234,30 @@ function ProviderCard({
return ( return (
<Card className={cn(isDefault && 'ring-2 ring-primary')}> <Card className={cn(isDefault && 'ring-2 ring-primary')}>
<CardHeader className="pb-3"> <CardContent className="p-4">
<div className="flex items-start justify-between"> {/* Top row: icon + name + toggle */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl">{typeInfo?.icon || '⚙️'}</span> <span className="text-xl">{typeInfo?.icon || '⚙️'}</span>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-lg">{provider.name}</CardTitle> <span className="font-semibold">{provider.name}</span>
{isDefault && ( {isDefault && (
<Badge variant="default" className="text-xs">Default</Badge> <Badge variant="default" className="text-xs">Default</Badge>
)} )}
</div> </div>
<CardDescription className="capitalize">{provider.type}</CardDescription> <span className="text-xs text-muted-foreground capitalize">{provider.type}</span>
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<Switch <Switch
checked={provider.enabled} checked={provider.enabled}
onCheckedChange={onToggleEnabled} onCheckedChange={onToggleEnabled}
/> />
</div> </div>
</div>
</CardHeader> {/* Key row */}
<CardContent>
{isEditing ? ( {isEditing ? (
<div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>API Key</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<Input <Input
@@ -259,55 +265,55 @@ function ProviderCard({
placeholder={typeInfo?.placeholder} placeholder={typeInfo?.placeholder}
value={newKey} value={newKey}
onChange={(e) => setNewKey(e.target.value)} onChange={(e) => setNewKey(e.target.value)}
className="pr-10" className="pr-10 h-9 text-sm"
/> />
<button <button
type="button" type="button"
onClick={() => setShowKey(!showKey)} onClick={() => setShowKey(!showKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
> >
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button> </button>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm"
onClick={handleSaveKey} onClick={handleSaveKey}
disabled={!newKey || validating || saving} disabled={!newKey || validating || saving}
> >
{validating || saving ? ( {validating || saving ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
) : ( ) : (
<Check className="h-4 w-4" /> <Check className="h-3.5 w-3.5" />
)} )}
</Button> </Button>
<Button variant="ghost" onClick={onCancelEdit}> <Button variant="ghost" size="sm" onClick={onCancelEdit}>
<X className="h-4 w-4" /> <X className="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
</div>
) : ( ) : (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between rounded-md bg-muted/50 px-3 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 min-w-0">
<Key className="h-4 w-4 text-muted-foreground" /> <Key className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-mono"> <span className="text-sm font-mono text-muted-foreground truncate">
{provider.hasKey ? provider.keyMasked : 'No API key set'} {provider.hasKey ? shortenKeyDisplay(provider.keyMasked) : 'No API key set'}
</span> </span>
{provider.hasKey && ( {provider.hasKey && (
<Badge variant="secondary" className="text-xs">Configured</Badge> <Badge variant="secondary" className="text-xs shrink-0">Configured</Badge>
)} )}
</div> </div>
<div className="flex gap-1"> <div className="flex gap-0.5 shrink-0 ml-2">
{!isDefault && ( {!isDefault && (
<Button variant="ghost" size="icon" onClick={onSetDefault} title="Set as default"> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={onSetDefault} title="Set as default">
<Star className="h-4 w-4" /> <Star className="h-3.5 w-3.5" />
</Button> </Button>
)} )}
<Button variant="ghost" size="icon" onClick={onEdit} title="Edit API key"> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={onEdit} title="Edit API key">
<Edit className="h-4 w-4" /> <Edit className="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" onClick={onDelete} title="Delete provider"> <Button variant="ghost" size="icon" className="h-7 w-7" onClick={onDelete} title="Delete provider">
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -46,7 +46,7 @@ export function Settings() {
}; };
return ( return (
<div className="space-y-6 max-w-2xl"> <div className="space-y-6 p-6">
<div> <div>
<h1 className="text-2xl font-bold">Settings</h1> <h1 className="text-2xl font-bold">Settings</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">