mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-07 04:08:45 +02:00
fix: group models by provider and eliminate popup background rectangle
- Rewrite ModelSelector to group models by AI provider (OpenAI, Anthropic, Google) using providerMeta and ProviderIcon instead of grouping by tier; show tier as compact badge per model row - Remove all box-shadow from qp-panel and border state variants; on Windows transparent BrowserWindows, shadows paint on the transparent canvas creating a visible dark rectangle behind the rounded panel - Add transparent background to html element in quickprompt.html and CSS override via :has(body[data-quickprompt]) to ensure no ancestor paints a visible background Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useRef, useMemo } from 'react';
|
import { useEffect, useRef, useMemo } from 'react';
|
||||||
import { Check, Crown, Zap, Brain } from 'lucide-react';
|
import { Check, Brain } from 'lucide-react';
|
||||||
|
|
||||||
import type { ModelDefinition } from '@shared/domain/models';
|
import { ProviderIcon } from '@renderer/components/ProviderIcons';
|
||||||
|
import type { ModelDefinition, ModelProvider } from '@shared/domain/models';
|
||||||
|
import { providerMeta } from '@shared/domain/models';
|
||||||
import type { ReasoningEffort } from '@shared/domain/workflow';
|
import type { ReasoningEffort } from '@shared/domain/workflow';
|
||||||
|
|
||||||
interface ModelSelectorProps {
|
interface ModelSelectorProps {
|
||||||
@@ -13,12 +15,6 @@ interface ModelSelectorProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tierMeta: Record<string, { label: string; icon: typeof Crown; color: string; bg: string }> = {
|
|
||||||
premium: { label: 'Premium', icon: Crown, color: 'text-amber-400', bg: 'bg-amber-400/10' },
|
|
||||||
standard: { label: 'Standard', icon: Zap, color: 'text-[var(--color-text-accent)]', bg: 'bg-[var(--color-accent-muted)]' },
|
|
||||||
fast: { label: 'Fast', icon: Zap, color: 'text-emerald-400', bg: 'bg-emerald-400/10' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const reasoningLevels: { value: ReasoningEffort; label: string; description: string }[] = [
|
const reasoningLevels: { value: ReasoningEffort; label: string; description: string }[] = [
|
||||||
{ value: 'low', label: 'Low', description: 'Fast, concise' },
|
{ value: 'low', label: 'Low', description: 'Fast, concise' },
|
||||||
{ value: 'medium', label: 'Med', description: 'Balanced' },
|
{ value: 'medium', label: 'Med', description: 'Balanced' },
|
||||||
@@ -26,8 +22,8 @@ const reasoningLevels: { value: ReasoningEffort; label: string; description: str
|
|||||||
{ value: 'xhigh', label: 'Max', description: 'Exhaustive' },
|
{ value: 'xhigh', label: 'Max', description: 'Exhaustive' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ModelGroup {
|
interface ProviderGroup {
|
||||||
tier: string;
|
provider: ModelProvider | 'other';
|
||||||
label: string;
|
label: string;
|
||||||
models: ModelDefinition[];
|
models: ModelDefinition[];
|
||||||
}
|
}
|
||||||
@@ -66,25 +62,38 @@ export function ModelSelector({
|
|||||||
const selectedModel = models.find((m) => m.id === selectedModelId);
|
const selectedModel = models.find((m) => m.id === selectedModelId);
|
||||||
const supportedEfforts = selectedModel?.supportedReasoningEfforts;
|
const supportedEfforts = selectedModel?.supportedReasoningEfforts;
|
||||||
|
|
||||||
// Group models by tier
|
// Group models by provider
|
||||||
const groups = useMemo((): ModelGroup[] => {
|
const groups = useMemo((): ProviderGroup[] => {
|
||||||
const tierOrder = ['premium', 'standard', 'fast', 'other'];
|
const providerOrder = providerMeta.map((p) => p.id);
|
||||||
|
const providerLabels = new Map(providerMeta.map((p) => [p.id, p.label]));
|
||||||
const grouped = new Map<string, ModelDefinition[]>();
|
const grouped = new Map<string, ModelDefinition[]>();
|
||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
const tier = model.tier ?? 'other';
|
const key = model.provider ?? 'other';
|
||||||
const list = grouped.get(tier) ?? [];
|
const list = grouped.get(key) ?? [];
|
||||||
list.push(model);
|
list.push(model);
|
||||||
grouped.set(tier, list);
|
grouped.set(key, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tierOrder
|
const result: ProviderGroup[] = [];
|
||||||
.filter((t) => grouped.has(t))
|
for (const providerId of providerOrder) {
|
||||||
.map((tier) => ({
|
const providerModels = grouped.get(providerId);
|
||||||
tier,
|
if (providerModels) {
|
||||||
label: tier === 'other' ? 'Other' : tierMeta[tier]?.label ?? tier,
|
result.push({
|
||||||
models: grouped.get(tier)!,
|
provider: providerId,
|
||||||
}));
|
label: providerLabels.get(providerId) ?? providerId,
|
||||||
|
models: providerModels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any models without a known provider
|
||||||
|
const other = grouped.get('other');
|
||||||
|
if (other) {
|
||||||
|
result.push({ provider: 'other', label: 'Other', models: other });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}, [models]);
|
}, [models]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,54 +103,63 @@ export function ModelSelector({
|
|||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="Select model"
|
aria-label="Select model"
|
||||||
>
|
>
|
||||||
{/* Model list — grouped by tier */}
|
{/* Model list — grouped by provider */}
|
||||||
<div className="max-h-[280px] overflow-y-auto overscroll-contain p-1.5">
|
<div className="max-h-[280px] overflow-y-auto overscroll-contain p-1.5">
|
||||||
{groups.map((group, gi) => {
|
{groups.map((group, gi) => (
|
||||||
const meta = tierMeta[group.tier];
|
<div key={group.provider}>
|
||||||
const TierIcon = meta?.icon;
|
{gi > 0 && <div className="mx-2 my-1 border-t border-[var(--color-border-subtle)]/50" />}
|
||||||
|
|
||||||
return (
|
{/* Provider header */}
|
||||||
<div key={group.tier}>
|
<div className="flex items-center gap-1.5 px-2.5 pt-2 pb-1">
|
||||||
{gi > 0 && <div className="mx-2 my-1 border-t border-[var(--color-border-subtle)]/50" />}
|
{group.provider !== 'other' && (
|
||||||
|
<ProviderIcon provider={group.provider} className="size-3" />
|
||||||
{/* Group header */}
|
)}
|
||||||
<div className="flex items-center gap-1.5 px-2.5 pt-2 pb-1">
|
<span className="text-[10px] font-semibold tracking-wider text-[var(--color-text-muted)] uppercase">
|
||||||
{TierIcon && <TierIcon className={`size-2.5 ${meta.color}`} />}
|
{group.label}
|
||||||
<span className={`text-[10px] font-semibold tracking-wider uppercase ${meta?.color ?? 'text-[var(--color-text-muted)]'}`}>
|
</span>
|
||||||
{group.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Models in this group */}
|
|
||||||
{group.models.map((model) => {
|
|
||||||
const isSelected = model.id === selectedModelId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={model.id}
|
|
||||||
onClick={() => onSelect(model)}
|
|
||||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-[7px] text-left transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-[var(--color-accent-muted)] text-[var(--color-text-primary)]'
|
|
||||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)]'
|
|
||||||
}`}
|
|
||||||
type="button"
|
|
||||||
role="option"
|
|
||||||
aria-selected={isSelected}
|
|
||||||
>
|
|
||||||
<span className="flex-1 truncate text-[12px] font-medium">{model.name}</span>
|
|
||||||
|
|
||||||
{model.supportedReasoningEfforts?.length ? (
|
|
||||||
<Brain className="size-3 flex-none text-[var(--color-text-muted)]/60" aria-label="Supports reasoning" />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isSelected && <Check className="size-3.5 flex-none text-[var(--color-accent)]" />}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* Models in this provider group */}
|
||||||
|
{group.models.map((model) => {
|
||||||
|
const isSelected = model.id === selectedModelId;
|
||||||
|
const tierLabel = model.tier === 'premium' ? 'PRO' : model.tier === 'fast' ? 'FAST' : undefined;
|
||||||
|
const tierColor = model.tier === 'premium'
|
||||||
|
? 'text-amber-400 bg-amber-400/10'
|
||||||
|
: model.tier === 'fast'
|
||||||
|
? 'text-emerald-400 bg-emerald-400/10'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={model.id}
|
||||||
|
onClick={() => onSelect(model)}
|
||||||
|
className={`flex w-full items-center gap-2 rounded-lg px-3 py-[7px] text-left transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-[var(--color-accent-muted)] text-[var(--color-text-primary)]'
|
||||||
|
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)]'
|
||||||
|
}`}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
>
|
||||||
|
<span className="flex-1 truncate text-[12px] font-medium">{model.name}</span>
|
||||||
|
|
||||||
|
{tierLabel && (
|
||||||
|
<span className={`rounded-[4px] px-1.5 py-px text-[8px] font-bold tracking-wider ${tierColor}`}>
|
||||||
|
{tierLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{model.supportedReasoningEfforts?.length ? (
|
||||||
|
<Brain className="size-3 flex-none text-[var(--color-text-muted)]/60" aria-label="Supports reasoning" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isSelected && <Check className="size-3.5 flex-none text-[var(--color-accent)]" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reasoning effort — only shown when selected model supports it */}
|
{/* Reasoning effort — only shown when selected model supports it */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" style="background: transparent;">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta
|
||||||
|
|||||||
+6
-19
@@ -708,13 +708,14 @@ body {
|
|||||||
/* Override body/root backgrounds when inside the quickprompt window so the
|
/* Override body/root backgrounds when inside the quickprompt window so the
|
||||||
transparent BrowserWindow doesn't show a colored rectangle behind the
|
transparent BrowserWindow doesn't show a colored rectangle behind the
|
||||||
rounded panel. The data attribute is set on the <body> in quickprompt.html. */
|
rounded panel. The data attribute is set on the <body> in quickprompt.html. */
|
||||||
body[data-quickprompt] {
|
body[data-quickprompt],
|
||||||
|
body[data-quickprompt] #root {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-quickprompt] #root {
|
html:has(body[data-quickprompt]) {
|
||||||
min-height: 0 !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qp-container {
|
.qp-container {
|
||||||
@@ -723,10 +724,8 @@ body[data-quickprompt] #root {
|
|||||||
|
|
||||||
.qp-panel {
|
.qp-panel {
|
||||||
background: var(--color-surface-1);
|
background: var(--color-surface-1);
|
||||||
box-shadow:
|
/* No box-shadow — on Windows transparent windows, shadows paint visibly
|
||||||
0 24px 80px rgba(0, 0, 0, 0.6),
|
on the transparent canvas and create a visible dark rectangle. */
|
||||||
0 8px 24px rgba(0, 0, 0, 0.4),
|
|
||||||
0 0 1px rgba(255, 255, 255, 0.08) inset;
|
|
||||||
transition: border-color 0.3s ease;
|
transition: border-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,27 +736,15 @@ body[data-quickprompt] #root {
|
|||||||
|
|
||||||
.qp-border-complete {
|
.qp-border-complete {
|
||||||
border: 1px solid var(--color-border-glow);
|
border: 1px solid var(--color-border-glow);
|
||||||
box-shadow:
|
|
||||||
0 24px 80px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 20px rgba(36, 92, 249, 0.06),
|
|
||||||
0 0 1px rgba(255, 255, 255, 0.06) inset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animated breathing border during streaming */
|
/* Animated breathing border during streaming */
|
||||||
@keyframes qp-border-breathe {
|
@keyframes qp-border-breathe {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
border-color: rgba(36, 92, 249, 0.35);
|
border-color: rgba(36, 92, 249, 0.35);
|
||||||
box-shadow:
|
|
||||||
0 24px 80px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 20px rgba(36, 92, 249, 0.08),
|
|
||||||
0 0 1px rgba(255, 255, 255, 0.06) inset;
|
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
border-color: rgba(138, 41, 230, 0.4);
|
border-color: rgba(138, 41, 230, 0.4);
|
||||||
box-shadow:
|
|
||||||
0 24px 80px rgba(0, 0, 0, 0.55),
|
|
||||||
0 0 32px rgba(138, 41, 230, 0.1),
|
|
||||||
0 0 1px rgba(255, 255, 255, 0.06) inset;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user