chore: ran all linting

This commit is contained in:
Anish Sarkar 2026-02-06 18:22:19 +05:30
parent 2470fb70a6
commit 76e7ddee2f
22 changed files with 638 additions and 433 deletions

View file

@ -4,7 +4,13 @@ import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";
export const Logo = ({ className, disableLink = false }: { className?: string; disableLink?: boolean }) => {
export const Logo = ({
className,
disableLink = false,
}: {
className?: string;
disableLink?: boolean;
}) => {
const image = (
<Image
src="/icon-128.svg"

View file

@ -100,10 +100,10 @@ export const UserMessage: FC = () => {
const UserActionBar: FC = () => {
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
// Get current message ID
const currentMessageId = useAssistantState(({ message }) => message?.id);
// Find the last user message ID in the thread (computed once, memoized by selector)
const lastUserMessageId = useAssistantState(({ thread }) => {
const messages = thread.messages;
@ -117,7 +117,7 @@ const UserActionBar: FC = () => {
// Simple comparison - no iteration needed per message
const isLastUserMessage = currentMessageId === lastUserMessageId;
// Show edit button only on the last user message and when thread is not running
const canEdit = isLastUserMessage && !isThreadRunning;

View file

@ -526,7 +526,9 @@ export function LayoutDataProvider({
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
// Invalidate thread detail for breadcrumb update
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId, "detail", String(chatToRename.id)] });
queryClient.invalidateQueries({
queryKey: ["threads", searchSpaceId, "detail", String(chatToRename.id)],
});
} catch (error) {
console.error("Error renaming thread:", error);
toast.error(tSidebar("error_renaming_chat") || "Failed to rename chat");

View file

@ -1,6 +1,13 @@
"use client";
import { ArchiveIcon, MessageSquare, MoreHorizontal, PencilIcon, RotateCcwIcon, Trash2 } from "lucide-react";
import {
ArchiveIcon,
MessageSquare,
MoreHorizontal,
PencilIcon,
RotateCcwIcon,
Trash2,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import {
@ -59,26 +66,26 @@ export function ChatListItem({
<span className="sr-only">{t("more_options")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right">
{onRename && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onRename();
}}
>
<PencilIcon className="mr-2 h-4 w-4" />
<span>{t("rename") || "Rename"}</span>
</DropdownMenuItem>
)}
{onArchive && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onArchive();
}}
>
{archived ? (
<DropdownMenuContent align="end" side="right">
{onRename && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onRename();
}}
>
<PencilIcon className="mr-2 h-4 w-4" />
<span>{t("rename") || "Rename"}</span>
</DropdownMenuItem>
)}
{onArchive && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onArchive();
}}
>
{archived ? (
<>
<RotateCcwIcon className="mr-2 h-4 w-4" />
<span>{t("unarchive") || "Restore"}</span>

View file

@ -219,7 +219,7 @@ export function InboxSidebar({
// Server-side search query (enabled only when user is typing a search)
// Determines which notification types to search based on active tab
const searchTypeFilter = activeTab === "comments" ? "new_mention" as const : undefined;
const searchTypeFilter = activeTab === "comments" ? ("new_mention" as const) : undefined;
const { data: searchResponse, isLoading: isSearchLoading } = useQuery({
queryKey: cacheKeys.notifications.search(searchSpaceId, debouncedSearch.trim(), activeTab),
queryFn: () =>
@ -288,8 +288,10 @@ export function InboxSidebar({
// Pagination switches based on active tab
const loading = activeTab === "comments" ? mentions.loading : status.loading;
const loadingMore = activeTab === "comments" ? (mentions.loadingMore ?? false) : (status.loadingMore ?? false);
const hasMore = activeTab === "comments" ? (mentions.hasMore ?? false) : (status.hasMore ?? false);
const loadingMore =
activeTab === "comments" ? (mentions.loadingMore ?? false) : (status.loadingMore ?? false);
const hasMore =
activeTab === "comments" ? (mentions.hasMore ?? false) : (status.hasMore ?? false);
const loadMore = activeTab === "comments" ? mentions.loadMore : status.loadMore;
// Get unique connector types from status items for filtering
@ -319,9 +321,7 @@ export function InboxSidebar({
// When not searching: use Electric real-time items (fast, local)
const filteredItems = useMemo(() => {
// In search mode, use API results
let items: InboxItem[] = isSearchMode
? (searchResponse?.items ?? [])
: displayItems;
let items: InboxItem[] = isSearchMode ? (searchResponse?.items ?? []) : displayItems;
// For status tab search results, filter to status-specific types
if (isSearchMode && activeTab === "status") {
@ -926,49 +926,49 @@ export function InboxSidebar({
</TabsList>
</Tabs>
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{(isSearchMode ? isSearchLoading : loading) ? (
<div className="space-y-2">
{activeTab === "comments"
? /* Comments skeleton: avatar + two-line text + time */
[85, 60, 90, 70, 50, 75].map((titleWidth, i) => (
<div
key={`skeleton-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" />
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{(isSearchMode ? isSearchLoading : loading) ? (
<div className="space-y-2">
{activeTab === "comments"
? /* Comments skeleton: avatar + two-line text + time */
[85, 60, 90, 70, 50, 75].map((titleWidth, i) => (
<div
key={`skeleton-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" />
</div>
<Skeleton className="h-3 w-6 shrink-0 rounded" />
</div>
<Skeleton className="h-3 w-6 shrink-0 rounded" />
</div>
))
: /* Status skeleton: status icon circle + two-line text + time */
[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
<div
key={`skeleton-status-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[60%] rounded" />
))
: /* Status skeleton: status icon circle + two-line text + time */
[75, 90, 55, 80, 65, 85].map((titleWidth, i) => (
<div
key={`skeleton-status-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[60%] rounded" />
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Skeleton className="h-3 w-6 rounded" />
<Skeleton className="h-2 w-2 rounded-full" />
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Skeleton className="h-3 w-6 rounded" />
<Skeleton className="h-2 w-2 rounded-full" />
</div>
</div>
))}
</div>
) : filteredItems.length > 0 ? (
<div className="space-y-2">
{filteredItems.map((item, index) => {
const isMarkingAsRead = markingAsReadId === item.id;
// Place prefetch trigger on 5th item from end (only when not searching)
const isPrefetchTrigger =
!isSearchMode && hasMore && index === filteredItems.length - 5;
))}
</div>
) : filteredItems.length > 0 ? (
<div className="space-y-2">
{filteredItems.map((item, index) => {
const isMarkingAsRead = markingAsReadId === item.id;
// Place prefetch trigger on 5th item from end (only when not searching)
const isPrefetchTrigger =
!isSearchMode && hasMore && index === filteredItems.length - 5;
return (
<div
@ -1023,54 +1023,53 @@ export function InboxSidebar({
</div>
);
})}
{/* Fallback trigger at the very end if less than 5 items and not searching */}
{!isSearchMode && filteredItems.length < 5 && hasMore && (
<div ref={prefetchTriggerRef} className="h-1" />
)}
{/* Loading more skeletons at the bottom during infinite scroll */}
{loadingMore && (
activeTab === "comments"
? [80, 60, 90].map((titleWidth, i) => (
<div
key={`loading-more-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" />
</div>
<Skeleton className="h-3 w-6 shrink-0 rounded" />
</div>
))
: [70, 85, 55].map((titleWidth, i) => (
<div
key={`loading-more-status-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[60%] rounded" />
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Skeleton className="h-3 w-6 rounded" />
<Skeleton className="h-2 w-2 rounded-full" />
</div>
</div>
))
)}
{/* Fallback trigger at the very end if less than 5 items and not searching */}
{!isSearchMode && filteredItems.length < 5 && hasMore && (
<div ref={prefetchTriggerRef} className="h-1" />
)}
{/* Loading more skeletons at the bottom during infinite scroll */}
{loadingMore &&
(activeTab === "comments"
? [80, 60, 90].map((titleWidth, i) => (
<div
key={`loading-more-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" />
</div>
<Skeleton className="h-3 w-6 shrink-0 rounded" />
</div>
))
: [70, 85, 55].map((titleWidth, i) => (
<div
key={`loading-more-status-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
<Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[60%] rounded" />
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Skeleton className="h-3 w-6 rounded" />
<Skeleton className="h-2 w-2 rounded-full" />
</div>
</div>
)))}
</div>
) : isSearchMode ? (
<div className="text-center py-8">
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
<p className="text-sm text-muted-foreground">
{t("no_results_found") || "No results found"}
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
{t("try_different_search") || "Try a different search term"}
</p>
</div>
) : isSearchMode ? (
<div className="text-center py-8">
<Search className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
<p className="text-sm text-muted-foreground">
{t("no_results_found") || "No results found"}
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
{t("try_different_search") || "Try a different search term"}
</p>
</div>
) : (
<div className="text-center py-8">
{activeTab === "comments" ? (

View file

@ -1,6 +1,16 @@
"use client";
import { Check, ChevronUp, Languages, Laptop, Loader2, LogOut, Moon, Settings, Sun } from "lucide-react";
import {
Check,
ChevronUp,
Languages,
Laptop,
Loader2,
LogOut,
Moon,
Settings,
Sun,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import {

View file

@ -155,7 +155,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
<TooltipContent>Share settings</TooltipContent>
</Tooltip>
<PopoverContent
<PopoverContent
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60"
align="end"
sideOffset={8}
@ -243,7 +243,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
</PopoverContent>
</Popover>
{/* Globe indicator when public snapshots exist - clicks to settings */}
{/* Globe indicator when public snapshots exist - clicks to settings */}
{hasPublicSnapshots && (
<Tooltip>
<TooltipTrigger asChild>

View file

@ -179,7 +179,17 @@ export function ImageConfigSidebar({
} finally {
setIsSubmitting(false);
}
}, [mode, isGlobal, config, formData, searchSpaceId, createConfig, updateConfig, updatePreferences, onOpenChange]);
}, [
mode,
isGlobal,
config,
formData,
searchSpaceId,
createConfig,
updateConfig,
updatePreferences,
onOpenChange,
]);
const handleUseGlobalConfig = useCallback(async () => {
if (!config || !isGlobal) return;
@ -297,11 +307,16 @@ export function ImageConfigSidebar({
<Alert className="mb-6 border-violet-500/30 bg-violet-500/5">
<Shuffle className="size-4 text-violet-500" />
<AlertDescription className="text-sm text-violet-700 dark:text-violet-400">
Auto mode distributes image generation requests across all configured providers for optimal performance and rate limit protection.
Auto mode distributes image generation requests across all configured
providers for optimal performance and rate limit protection.
</AlertDescription>
</Alert>
<div className="flex gap-3 pt-4 border-t border-border/50">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
<Button
variant="outline"
className="flex-1"
onClick={() => onOpenChange(false)}
>
Close
</Button>
<Button
@ -327,12 +342,16 @@ export function ImageConfigSidebar({
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Name</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Name
</div>
<p className="text-sm font-medium">{config.name}</p>
</div>
{config.description && (
<div className="space-y-1.5">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Description</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Description
</div>
<p className="text-sm text-muted-foreground">{config.description}</p>
</div>
)}
@ -340,20 +359,32 @@ export function ImageConfigSidebar({
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Provider</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Provider
</div>
<p className="text-sm font-medium">{config.provider}</p>
</div>
<div className="space-y-1.5">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Model</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Model
</div>
<p className="text-sm font-medium font-mono">{config.model_name}</p>
</div>
</div>
</div>
<div className="flex gap-3 pt-6 border-t border-border/50 mt-6">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
<Button
variant="outline"
className="flex-1"
onClick={() => onOpenChange(false)}
>
Close
</Button>
<Button className="flex-1 gap-2" onClick={handleUseGlobalConfig} disabled={isSubmitting}>
<Button
className="flex-1 gap-2"
onClick={handleUseGlobalConfig}
disabled={isSubmitting}
>
{isSubmitting ? "Loading..." : "Use This Model"}
</Button>
</div>
@ -379,7 +410,9 @@ export function ImageConfigSidebar({
<Input
placeholder="Optional description"
value={formData.description}
onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
onChange={(e) =>
setFormData((p) => ({ ...p, description: e.target.value }))
}
/>
</div>
@ -390,7 +423,9 @@ export function ImageConfigSidebar({
<Label className="text-sm font-medium">Provider *</Label>
<Select
value={formData.provider}
onValueChange={(val) => setFormData((p) => ({ ...p, provider: val, model_name: "" }))}
onValueChange={(val) =>
setFormData((p) => ({ ...p, provider: val, model_name: "" }))
}
>
<SelectTrigger>
<SelectValue placeholder="Select a provider" />
@ -414,7 +449,11 @@ export function ImageConfigSidebar({
{suggestedModels.length > 0 ? (
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="w-full justify-between font-normal">
<Button
variant="outline"
role="combobox"
className="w-full justify-between font-normal"
>
{formData.model_name || "Select or type a model..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@ -424,11 +463,15 @@ export function ImageConfigSidebar({
<CommandInput
placeholder="Search or type model..."
value={formData.model_name}
onValueChange={(val) => setFormData((p) => ({ ...p, model_name: val }))}
onValueChange={(val) =>
setFormData((p) => ({ ...p, model_name: val }))
}
/>
<CommandList>
<CommandEmpty>
<span className="text-xs text-muted-foreground">Type a custom model name</span>
<span className="text-xs text-muted-foreground">
Type a custom model name
</span>
</CommandEmpty>
<CommandGroup>
{suggestedModels.map((m) => (
@ -440,9 +483,18 @@ export function ImageConfigSidebar({
setModelComboboxOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", formData.model_name === m.value ? "opacity-100" : "opacity-0")} />
<Check
className={cn(
"mr-2 h-4 w-4",
formData.model_name === m.value
? "opacity-100"
: "opacity-0"
)}
/>
<span className="font-mono text-sm">{m.value}</span>
<span className="ml-2 text-xs text-muted-foreground">{m.label}</span>
<span className="ml-2 text-xs text-muted-foreground">
{m.label}
</span>
</CommandItem>
))}
</CommandGroup>
@ -454,7 +506,9 @@ export function ImageConfigSidebar({
<Input
placeholder="e.g., dall-e-3"
value={formData.model_name}
onChange={(e) => setFormData((p) => ({ ...p, model_name: e.target.value }))}
onChange={(e) =>
setFormData((p) => ({ ...p, model_name: e.target.value }))
}
/>
)}
</div>
@ -489,14 +543,20 @@ export function ImageConfigSidebar({
<Input
placeholder="2024-02-15-preview"
value={formData.api_version}
onChange={(e) => setFormData((p) => ({ ...p, api_version: e.target.value }))}
onChange={(e) =>
setFormData((p) => ({ ...p, api_version: e.target.value }))
}
/>
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-4 border-t">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>
<Button
variant="outline"
className="flex-1"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button

View file

@ -54,8 +54,7 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const { data: globalConfigs, isLoading: globalLoading } =
useAtomValue(globalImageGenConfigsAtom);
const { data: globalConfigs, isLoading: globalLoading } = useAtomValue(globalImageGenConfigsAtom);
const { data: userConfigs, isLoading: userLoading } = useAtomValue(imageGenConfigsAtom);
const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
@ -225,59 +224,59 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
<Globe className="size-3.5" />
Global Image Models
</div>
{filteredGlobal.map((config) => {
const isSelected = currentConfig?.id === config.id;
const isAuto = "is_auto_mode" in config && config.is_auto_mode;
return (
<CommandItem
key={`g-${config.id}`}
value={`g-${config.id}`}
onSelect={() => handleSelect(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80",
isAuto && "border border-violet-200 dark:border-violet-800/50"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="shrink-0">
{isAuto ? (
<Shuffle className="size-4 text-violet-500" />
) : (
<ImageIcon className="size-4 text-teal-500" />
{filteredGlobal.map((config) => {
const isSelected = currentConfig?.id === config.id;
const isAuto = "is_auto_mode" in config && config.is_auto_mode;
return (
<CommandItem
key={`g-${config.id}`}
value={`g-${config.id}`}
onSelect={() => handleSelect(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80",
isAuto && "border border-violet-200 dark:border-violet-800/50"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="shrink-0">
{isAuto ? (
<Shuffle className="size-4 text-violet-500" />
) : (
<ImageIcon className="size-4 text-teal-500" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium truncate">{config.name}</span>
{isAuto && (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-0"
>
Recommended
</Badge>
)}
{isSelected && <Check className="size-3.5 text-primary shrink-0" />}
</div>
<span className="text-xs text-muted-foreground truncate block">
{isAuto ? "Auto load balancing" : config.model_name}
</span>
</div>
{onEdit && (
<ChevronRight
className="size-3.5 text-muted-foreground shrink-0 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
onClick={(e) => {
e.stopPropagation();
setOpen(false);
onEdit(config, true);
}}
/>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium truncate">{config.name}</span>
{isAuto && (
<Badge
variant="secondary"
className="text-[9px] px-1 py-0 h-3.5 bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-0"
>
Recommended
</Badge>
)}
{isSelected && <Check className="size-3.5 text-primary shrink-0" />}
</div>
<span className="text-xs text-muted-foreground truncate block">
{isAuto ? "Auto load balancing" : config.model_name}
</span>
</div>
{onEdit && (
<ChevronRight
className="size-3.5 text-muted-foreground shrink-0 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
onClick={(e) => {
e.stopPropagation();
setOpen(false);
onEdit(config, true);
}}
/>
)}
</div>
</CommandItem>
);
})}
</CommandItem>
);
})}
</CommandGroup>
)}
@ -290,51 +289,49 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
<User className="size-3.5" />
Your Image Models
</div>
{filteredUser.map((config) => {
const isSelected = currentConfig?.id === config.id;
return (
<CommandItem
key={`u-${config.id}`}
value={`u-${config.id}`}
onSelect={() => handleSelect(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80"
)}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="shrink-0">
<ImageIcon className="size-4 text-teal-500" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium truncate">{config.name}</span>
{isSelected && (
<Check className="size-3.5 text-primary shrink-0" />
)}
</div>
<span className="text-xs text-muted-foreground truncate block">
{config.model_name}
</span>
</div>
{onEdit && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
setOpen(false);
onEdit(config, false);
}}
>
<Edit3 className="size-3.5 text-muted-foreground" />
</Button>
{filteredUser.map((config) => {
const isSelected = currentConfig?.id === config.id;
return (
<CommandItem
key={`u-${config.id}`}
value={`u-${config.id}`}
onSelect={() => handleSelect(config.id)}
className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80"
)}
</div>
</CommandItem>
);
})}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="shrink-0">
<ImageIcon className="size-4 text-teal-500" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-medium truncate">{config.name}</span>
{isSelected && <Check className="size-3.5 text-primary shrink-0" />}
</div>
<span className="text-xs text-muted-foreground truncate block">
{config.model_name}
</span>
</div>
{onEdit && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation();
setOpen(false);
onEdit(config, false);
}}
>
<Edit3 className="size-3.5 text-muted-foreground" />
</Button>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</>
)}

View file

@ -392,8 +392,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</CommandGroup>
)}
{/* Add New Config Button */}
<div className="p-2 bg-muted/20">
{/* Add New Config Button */}
<div className="p-2 bg-muted/20">
<Button
variant="ghost"
size="sm"

View file

@ -184,7 +184,12 @@ export function Pricing({
</div>
<p className="text-xs leading-5 text-muted-foreground">
{plan.billingText ?? (isNaN(Number(plan.price)) ? "" : isMonthly ? "billed monthly" : "billed annually")}
{plan.billingText ??
(isNaN(Number(plan.price))
? ""
: isMonthly
? "billed monthly"
: "billed annually")}
</p>
<ul className="mt-5 gap-2 flex flex-col">

View file

@ -95,16 +95,29 @@ const item = {
export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
// Image gen config atoms
const { mutateAsync: createConfig, isPending: isCreating, error: createError } =
useAtomValue(createImageGenConfigMutationAtom);
const { mutateAsync: updateConfig, isPending: isUpdating, error: updateError } =
useAtomValue(updateImageGenConfigMutationAtom);
const { mutateAsync: deleteConfig, isPending: isDeleting, error: deleteError } =
useAtomValue(deleteImageGenConfigMutationAtom);
const {
mutateAsync: createConfig,
isPending: isCreating,
error: createError,
} = useAtomValue(createImageGenConfigMutationAtom);
const {
mutateAsync: updateConfig,
isPending: isUpdating,
error: updateError,
} = useAtomValue(updateImageGenConfigMutationAtom);
const {
mutateAsync: deleteConfig,
isPending: isDeleting,
error: deleteError,
} = useAtomValue(deleteImageGenConfigMutationAtom);
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
const { data: userConfigs, isFetching: configsLoading, error: fetchError, refetch: refreshConfigs } =
useAtomValue(imageGenConfigsAtom);
const {
data: userConfigs,
isFetching: configsLoading,
error: fetchError,
refetch: refreshConfigs,
} = useAtomValue(imageGenConfigsAtom);
const { data: globalConfigs = [], isFetching: globalLoading } =
useAtomValue(globalImageGenConfigsAtom);
const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
@ -249,7 +262,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
data: {
image_generation_config_id:
typeof selectedPrefId === "string"
? selectedPrefId ? parseInt(selectedPrefId) : undefined
? selectedPrefId
? parseInt(selectedPrefId)
: undefined
: selectedPrefId,
},
});
@ -289,7 +304,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Errors */}
<AnimatePresence>
{errors.map((err) => (
<motion.div key={err?.message} initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}>
<motion.div
key={err?.message}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<Alert variant="destructive" className="py-3">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">{err?.message}</AlertDescription>
@ -304,7 +324,8 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Sparkles className="h-3 w-3 md:h-4 md:w-4 text-teal-600 dark:text-teal-400 shrink-0" />
<AlertDescription className="text-teal-800 dark:text-teal-200 text-xs md:text-sm">
<span className="font-medium">
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global image model(s)
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length} global
image model(s)
</span>{" "}
available from your administrator.
</AlertDescription>
@ -342,18 +363,27 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</SelectItem>
{globalConfigs.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Global</div>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Global
</div>
{globalConfigs.map((c) => {
const isAuto = "is_auto_mode" in c && c.is_auto_mode;
return (
<SelectItem key={`g-${c.id}`} value={c.id.toString()}>
<div className="flex items-center gap-2">
{isAuto ? (
<Badge variant="outline" className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-violet-200">
<Shuffle className="size-3 mr-1" />AUTO
<Badge
variant="outline"
className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-violet-200"
>
<Shuffle className="size-3 mr-1" />
AUTO
</Badge>
) : (
<Badge variant="outline" className="text-xs bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 border-teal-200">
<Badge
variant="outline"
className="text-xs bg-teal-50 text-teal-700 dark:bg-teal-900/30 dark:text-teal-300 border-teal-200"
>
{c.provider}
</Badge>
)}
@ -366,11 +396,15 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
{(userConfigs?.length ?? 0) > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">Your Models</div>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Your Models
</div>
{userConfigs?.map((c) => (
<SelectItem key={`u-${c.id}`} value={c.id.toString()}>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">{c.provider}</Badge>
<Badge variant="outline" className="text-xs">
{c.provider}
</Badge>
<span>{c.name}</span>
<span className="text-muted-foreground">({c.model_name})</span>
</div>
@ -382,10 +416,23 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</Select>
{hasPrefChanges && (
<div className="flex gap-2 pt-1">
<Button size="sm" onClick={handleSavePref} disabled={isSavingPref} className="text-xs h-8">
<Button
size="sm"
onClick={handleSavePref}
disabled={isSavingPref}
className="text-xs h-8"
>
{isSavingPref ? "Saving..." : "Save"}
</Button>
<Button size="sm" variant="outline" onClick={() => { setSelectedPrefId(preferences.image_generation_config_id ?? ""); setHasPrefChanges(false); }} className="text-xs h-8">
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedPrefId(preferences.image_generation_config_id ?? "");
setHasPrefChanges(false);
}}
className="text-xs h-8"
>
Reset
</Button>
</div>
@ -409,7 +456,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<div className="space-y-4 md:space-y-6">
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<h3 className="text-lg md:text-xl font-semibold tracking-tight">Your Image Models</h3>
<Button onClick={openNewDialog} className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9">
<Button
onClick={openNewDialog}
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<Plus className="h-3 w-3 md:h-4 md:w-4" />
Add Image Model
</Button>
@ -435,7 +485,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<motion.div variants={container} initial="hidden" animate="show" className="grid gap-4">
<AnimatePresence mode="popLayout">
{userConfigs?.map((config) => (
<motion.div key={config.id} variants={item} layout exit={{ opacity: 0, scale: 0.95 }}>
<motion.div
key={config.id}
variants={item}
layout
exit={{ opacity: 0, scale: 0.95 }}
>
<Card className="group overflow-hidden hover:shadow-lg transition-all duration-300 border-muted-foreground/10 hover:border-teal-500/30">
<CardContent className="p-0">
<div className="flex">
@ -448,8 +503,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</div>
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center gap-1.5 flex-wrap">
<h4 className="text-sm md:text-base font-semibold truncate">{config.name}</h4>
<Badge variant="secondary" className="text-[9px] md:text-[10px] px-1.5 py-0.5 bg-teal-500/10 text-teal-700 dark:text-teal-300 border-teal-500/20">
<h4 className="text-sm md:text-base font-semibold truncate">
{config.name}
</h4>
<Badge
variant="secondary"
className="text-[9px] md:text-[10px] px-1.5 py-0.5 bg-teal-500/10 text-teal-700 dark:text-teal-300 border-teal-500/20"
>
{config.provider}
</Badge>
</div>
@ -457,7 +517,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{config.model_name}
</code>
{config.description && (
<p className="text-[10px] md:text-xs text-muted-foreground line-clamp-1">{config.description}</p>
<p className="text-[10px] md:text-xs text-muted-foreground line-clamp-1">
{config.description}
</p>
)}
<div className="flex items-center gap-1 text-[10px] md:text-xs text-muted-foreground pt-1">
<Clock className="h-2.5 w-2.5 md:h-3 md:w-3" />
@ -469,7 +531,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={() => openEditDialog(config)} className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground">
<Button
variant="ghost"
size="sm"
onClick={() => openEditDialog(config)}
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
>
<Edit3 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
@ -479,7 +546,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={() => setConfigToDelete(config)} className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive">
<Button
variant="ghost"
size="sm"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
@ -501,15 +573,30 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
{/* Create/Edit Dialog */}
<Dialog open={isDialogOpen} onOpenChange={(open) => { if (!open) { setIsDialogOpen(false); setEditingConfig(null); resetForm(); } }}>
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
if (!open) {
setIsDialogOpen(false);
setEditingConfig(null);
resetForm();
}
}}
>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{editingConfig ? <Edit3 className="w-5 h-5 text-teal-600" /> : <Plus className="w-5 h-5 text-teal-600" />}
{editingConfig ? (
<Edit3 className="w-5 h-5 text-teal-600" />
) : (
<Plus className="w-5 h-5 text-teal-600" />
)}
{editingConfig ? "Edit Image Model" : "Add Image Model"}
</DialogTitle>
<DialogDescription>
{editingConfig ? "Update your image generation model" : "Configure a new image generation model (DALL-E 3, GPT Image 1, etc.)"}
{editingConfig
? "Update your image generation model"
: "Configure a new image generation model (DALL-E 3, GPT Image 1, etc.)"}
</DialogDescription>
</DialogHeader>
@ -541,7 +628,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Label className="text-sm font-medium">Provider *</Label>
<Select
value={formData.provider}
onValueChange={(val) => setFormData((p) => ({ ...p, provider: val, model_name: "" }))}
onValueChange={(val) =>
setFormData((p) => ({ ...p, provider: val, model_name: "" }))
}
>
<SelectTrigger>
<SelectValue placeholder="Select a provider" />
@ -565,7 +654,11 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{suggestedModels.length > 0 ? (
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="w-full justify-between font-normal">
<Button
variant="outline"
role="combobox"
className="w-full justify-between font-normal"
>
{formData.model_name || "Select or type a model..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@ -579,7 +672,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
/>
<CommandList>
<CommandEmpty>
<span className="text-xs text-muted-foreground">Type a custom model name</span>
<span className="text-xs text-muted-foreground">
Type a custom model name
</span>
</CommandEmpty>
<CommandGroup>
{suggestedModels.map((m) => (
@ -591,7 +686,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
setModelComboboxOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", formData.model_name === m.value ? "opacity-100" : "opacity-0")} />
<Check
className={cn(
"mr-2 h-4 w-4",
formData.model_name === m.value ? "opacity-100" : "opacity-0"
)}
/>
<span className="font-mono text-sm">{m.value}</span>
<span className="ml-2 text-xs text-muted-foreground">{m.label}</span>
</CommandItem>
@ -650,14 +750,24 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Button
variant="outline"
className="flex-1"
onClick={() => { setIsDialogOpen(false); setEditingConfig(null); resetForm(); }}
onClick={() => {
setIsDialogOpen(false);
setEditingConfig(null);
resetForm();
}}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={handleFormSubmit}
disabled={isSubmitting || !formData.name || !formData.provider || !formData.model_name || !formData.api_key}
disabled={
isSubmitting ||
!formData.name ||
!formData.provider ||
!formData.model_name ||
!formData.api_key
}
>
{isSubmitting ? <Spinner size="sm" className="mr-2" /> : null}
{editingConfig ? "Save Changes" : "Create & Use"}
@ -668,7 +778,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={!!configToDelete} onOpenChange={(open) => !open && setConfigToDelete(null)}>
<AlertDialog
open={!!configToDelete}
onOpenChange={(open) => !open && setConfigToDelete(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
@ -676,13 +789,28 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Delete Image Model
</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete <span className="font-semibold text-foreground">{configToDelete?.name}</span>?
Are you sure you want to delete{" "}
<span className="font-semibold text-foreground">{configToDelete?.name}</span>?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{isDeleting ? <><Spinner size="sm" className="mr-2" />Deleting</> : <><Trash2 className="mr-2 h-4 w-4" />Delete</>}
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Spinner size="sm" className="mr-2" />
Deleting
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View file

@ -255,15 +255,15 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
</Alert>
)}
{/* Role Assignment Cards */}
{availableConfigs.length > 0 && (
<div className="grid gap-4 md:gap-6">
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
const IconComponent = role.icon;
const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
const assignedConfig = availableConfigs.find(
(config) => config.id === currentAssignment
);
{/* Role Assignment Cards */}
{availableConfigs.length > 0 && (
<div className="grid gap-4 md:gap-6">
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
const IconComponent = role.icon;
const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
const assignedConfig = availableConfigs.find(
(config) => config.id === currentAssignment
);
return (
<motion.div
@ -294,100 +294,100 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
</div>
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label className="text-xs md:text-sm font-medium">
Assign LLM Configuration:
</Label>
<Select
value={currentAssignment?.toString() || "unassigned"}
onValueChange={(value) => handleRoleAssignment(`${key}_llm_id`, value)}
>
<SelectTrigger className="h-9 md:h-10 text-xs md:text-sm">
<SelectValue placeholder="Select an LLM configuration" />
</SelectTrigger>
<SelectContent>
<SelectItem value="unassigned">
<span className="text-muted-foreground">Unassigned</span>
</SelectItem>
<div className="space-y-1.5 md:space-y-2">
<Label className="text-xs md:text-sm font-medium">
Assign LLM Configuration:
</Label>
<Select
value={currentAssignment?.toString() || "unassigned"}
onValueChange={(value) => handleRoleAssignment(`${key}_llm_id`, value)}
>
<SelectTrigger className="h-9 md:h-10 text-xs md:text-sm">
<SelectValue placeholder="Select an LLM configuration" />
</SelectTrigger>
<SelectContent>
<SelectItem value="unassigned">
<span className="text-muted-foreground">Unassigned</span>
</SelectItem>
{/* Global Configurations */}
{globalConfigs.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Global Configurations
</div>
{globalConfigs.map((config) => {
const isAutoMode =
"is_auto_mode" in config && config.is_auto_mode;
return (
<SelectItem key={config.id} value={config.id.toString()}>
<div className="flex items-center gap-2">
{isAutoMode ? (
<Badge
variant="outline"
className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-violet-200 dark:border-violet-700"
>
<Shuffle className="size-3 mr-1" />
AUTO
</Badge>
) : (
<Badge variant="outline" className="text-xs">
{config.provider}
</Badge>
)}
<span>{config.name}</span>
{!isAutoMode && (
<span className="text-muted-foreground">
({config.model_name})
</span>
)}
{isAutoMode ? (
<Badge
variant="secondary"
className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300"
>
Recommended
</Badge>
) : (
<Badge variant="secondary" className="text-xs">
🌐 Global
</Badge>
)}
</div>
</SelectItem>
);
})}
</>
)}
{/* Global Configurations */}
{globalConfigs.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Global Configurations
</div>
{globalConfigs.map((config) => {
const isAutoMode =
"is_auto_mode" in config && config.is_auto_mode;
return (
<SelectItem key={config.id} value={config.id.toString()}>
<div className="flex items-center gap-2">
{isAutoMode ? (
<Badge
variant="outline"
className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300 border-violet-200 dark:border-violet-700"
>
<Shuffle className="size-3 mr-1" />
AUTO
</Badge>
) : (
<Badge variant="outline" className="text-xs">
{config.provider}
</Badge>
)}
<span>{config.name}</span>
{!isAutoMode && (
<span className="text-muted-foreground">
({config.model_name})
</span>
)}
{isAutoMode ? (
<Badge
variant="secondary"
className="text-xs bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300"
>
Recommended
</Badge>
) : (
<Badge variant="secondary" className="text-xs">
🌐 Global
</Badge>
)}
</div>
</SelectItem>
);
})}
</>
)}
{/* Custom Configurations */}
{newLLMConfigs.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Your Configurations
</div>
{newLLMConfigs
.filter(
(config) => config.id && config.id.toString().trim() !== ""
)
.map((config) => (
<SelectItem key={config.id} value={config.id.toString()}>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{config.provider}
</Badge>
<span>{config.name}</span>
<span className="text-muted-foreground">
({config.model_name})
</span>
</div>
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
{/* Custom Configurations */}
{newLLMConfigs.length > 0 && (
<>
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
Your Configurations
</div>
{newLLMConfigs
.filter(
(config) => config.id && config.id.toString().trim() !== ""
)
.map((config) => (
<SelectItem key={config.id} value={config.id.toString()}>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{config.provider}
</Badge>
<span>{config.name}</span>
<span className="text-muted-foreground">
({config.model_name})
</span>
</div>
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>
{assignedConfig && (
<div

View file

@ -345,9 +345,7 @@ export function Image({
variant="secondary"
className={cn(
"border-0 text-xs backdrop-blur-sm",
isGenerated
? "bg-primary/80 text-primary-foreground"
: "bg-black/60 text-white"
isGenerated ? "bg-primary/80 text-primary-foreground" : "bg-black/60 text-white"
)}
>
{isGenerated && <SparklesIcon className="size-3 mr-1" />}