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

@ -197,10 +197,9 @@ async def list_notifications(
# Filter by search query (case-insensitive title/message search) # Filter by search query (case-insensitive title/message search)
if search: if search:
search_term = f"%{search}%" search_term = f"%{search}%"
search_filter = ( search_filter = Notification.title.ilike(
Notification.title.ilike(search_term) search_term
| Notification.message.ilike(search_term) ) | Notification.message.ilike(search_term)
)
query = query.where(search_filter) query = query.where(search_filter)
count_query = count_query.where(search_filter) count_query = count_query.where(search_filter)

View file

@ -1005,9 +1005,7 @@ async def _process_circleback_meeting(
# Start Redis heartbeat for stale task detection # Start Redis heartbeat for stale task detection
_start_heartbeat(notification.id) _start_heartbeat(notification.id)
heartbeat_task = asyncio.create_task( heartbeat_task = asyncio.create_task(_run_heartbeat_loop(notification.id))
_run_heartbeat_loop(notification.id)
)
log_entry = await task_logger.log_task_start( log_entry = await task_logger.log_task_start(
task_name="process_circleback_meeting", task_name="process_circleback_meeting",

View file

@ -45,9 +45,8 @@ _redis_client: redis.Redis | None = None
# Error messages shown to users when tasks are interrupted # Error messages shown to users when tasks are interrupted
STALE_SYNC_ERROR_MESSAGE = "Sync was interrupted unexpectedly. Please retry." STALE_SYNC_ERROR_MESSAGE = "Sync was interrupted unexpectedly. Please retry."
STALE_PROCESSING_ERROR_MESSAGE = ( STALE_PROCESSING_ERROR_MESSAGE = "Syncing was interrupted unexpectedly. Please retry."
"Syncing was interrupted unexpectedly. Please retry."
)
def get_redis_client() -> redis.Redis: def get_redis_client() -> redis.Redis:
"""Get or create Redis client for heartbeat checking.""" """Get or create Redis client for heartbeat checking."""
@ -310,9 +309,7 @@ async def _cleanup_stale_document_processing_notifications():
in_progress_rows = result.fetchall() in_progress_rows = result.fetchall()
if not in_progress_rows: if not in_progress_rows:
logger.debug( logger.debug("No in-progress document processing notifications found")
"No in-progress document processing notifications found"
)
return return
# Check which ones are missing heartbeat keys in Redis # Check which ones are missing heartbeat keys in Redis
@ -389,9 +386,7 @@ async def _cleanup_stale_document_processing_notifications():
await session.rollback() await session.rollback()
async def _cleanup_stuck_non_connector_documents( async def _cleanup_stuck_non_connector_documents(session, document_ids: list[int]):
session, document_ids: list[int]
):
""" """
Mark specific non-connector documents stuck in pending/processing as failed. Mark specific non-connector documents stuck in pending/processing as failed.

View file

@ -372,11 +372,11 @@ export function DocumentsTableShell({
<Skeleton className="h-3 w-16" /> <Skeleton className="h-3 w-16" />
</TableHead> </TableHead>
)} )}
{columnVisibility.status && ( {columnVisibility.status && (
<TableHead className="w-14 text-center"> <TableHead className="w-14 text-center">
<Skeleton className="h-3 w-12 mx-auto" /> <Skeleton className="h-3 w-12 mx-auto" />
</TableHead> </TableHead>
)} )}
<TableHead className="w-10"> <TableHead className="w-10">
<span className="sr-only">Actions</span> <span className="sr-only">Actions</span>
</TableHead> </TableHead>
@ -544,14 +544,14 @@ export function DocumentsTableShell({
</SortableHeader> </SortableHeader>
</TableHead> </TableHead>
)} )}
{columnVisibility.status && ( {columnVisibility.status && (
<TableHead className="w-14"> <TableHead className="w-14">
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70"> <span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70">
<BadgeInfo size={14} className="opacity-60 text-muted-foreground" /> <BadgeInfo size={14} className="opacity-60 text-muted-foreground" />
Status Status
</span> </span>
</TableHead> </TableHead>
)} )}
<TableHead className="w-10"> <TableHead className="w-10">
<span className="sr-only">Actions</span> <span className="sr-only">Actions</span>
</TableHead> </TableHead>
@ -647,11 +647,11 @@ export function DocumentsTableShell({
</Tooltip> </Tooltip>
</TableCell> </TableCell>
)} )}
{columnVisibility.status && ( {columnVisibility.status && (
<TableCell className="w-14 py-2.5 text-center"> <TableCell className="w-14 py-2.5 text-center">
<StatusIndicator status={doc.status} /> <StatusIndicator status={doc.status} />
</TableCell> </TableCell>
)} )}
<TableCell className="w-10 py-2.5 text-center"> <TableCell className="w-10 py-2.5 text-center">
<RowActions <RowActions
document={doc} document={doc}

View file

@ -844,7 +844,12 @@ export default function NewChatPage() {
}); });
// Invalidate thread detail for breadcrumb update // Invalidate thread detail for breadcrumb update
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["threads", String(searchSpaceId), "detail", String(titleData.threadId)], queryKey: [
"threads",
String(searchSpaceId),
"detail",
String(titleData.threadId),
],
}); });
} }
break; break;

View file

@ -290,11 +290,11 @@ function SettingsContent({
<GeneralSettingsManager searchSpaceId={searchSpaceId} /> <GeneralSettingsManager searchSpaceId={searchSpaceId} />
)} )}
{activeSection === "models" && <ModelConfigManager searchSpaceId={searchSpaceId} />} {activeSection === "models" && <ModelConfigManager searchSpaceId={searchSpaceId} />}
{activeSection === "roles" && <LLMRoleManager searchSpaceId={searchSpaceId} />} {activeSection === "roles" && <LLMRoleManager searchSpaceId={searchSpaceId} />}
{activeSection === "image-models" && ( {activeSection === "image-models" && (
<ImageModelManager searchSpaceId={searchSpaceId} /> <ImageModelManager searchSpaceId={searchSpaceId} />
)} )}
{activeSection === "prompts" && <PromptConfigManager searchSpaceId={searchSpaceId} />} {activeSection === "prompts" && <PromptConfigManager searchSpaceId={searchSpaceId} />}
{activeSection === "public-links" && ( {activeSection === "public-links" && (
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} /> <PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
)} )}

View file

@ -4,7 +4,13 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { cn } from "@/lib/utils"; 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 = ( const image = (
<Image <Image
src="/icon-128.svg" src="/icon-128.svg"

View file

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

View file

@ -1,6 +1,13 @@
"use client"; "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 { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -59,26 +66,26 @@ export function ChatListItem({
<span className="sr-only">{t("more_options")}</span> <span className="sr-only">{t("more_options")}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right"> <DropdownMenuContent align="end" side="right">
{onRename && ( {onRename && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onRename(); onRename();
}} }}
> >
<PencilIcon className="mr-2 h-4 w-4" /> <PencilIcon className="mr-2 h-4 w-4" />
<span>{t("rename") || "Rename"}</span> <span>{t("rename") || "Rename"}</span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{onArchive && ( {onArchive && (
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onArchive(); onArchive();
}} }}
> >
{archived ? ( {archived ? (
<> <>
<RotateCcwIcon className="mr-2 h-4 w-4" /> <RotateCcwIcon className="mr-2 h-4 w-4" />
<span>{t("unarchive") || "Restore"}</span> <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) // Server-side search query (enabled only when user is typing a search)
// Determines which notification types to search based on active tab // 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({ const { data: searchResponse, isLoading: isSearchLoading } = useQuery({
queryKey: cacheKeys.notifications.search(searchSpaceId, debouncedSearch.trim(), activeTab), queryKey: cacheKeys.notifications.search(searchSpaceId, debouncedSearch.trim(), activeTab),
queryFn: () => queryFn: () =>
@ -288,8 +288,10 @@ export function InboxSidebar({
// Pagination switches based on active tab // Pagination switches based on active tab
const loading = activeTab === "comments" ? mentions.loading : status.loading; const loading = activeTab === "comments" ? mentions.loading : status.loading;
const loadingMore = activeTab === "comments" ? (mentions.loadingMore ?? false) : (status.loadingMore ?? false); const loadingMore =
const hasMore = activeTab === "comments" ? (mentions.hasMore ?? false) : (status.hasMore ?? false); 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; const loadMore = activeTab === "comments" ? mentions.loadMore : status.loadMore;
// Get unique connector types from status items for filtering // 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) // When not searching: use Electric real-time items (fast, local)
const filteredItems = useMemo(() => { const filteredItems = useMemo(() => {
// In search mode, use API results // In search mode, use API results
let items: InboxItem[] = isSearchMode let items: InboxItem[] = isSearchMode ? (searchResponse?.items ?? []) : displayItems;
? (searchResponse?.items ?? [])
: displayItems;
// For status tab search results, filter to status-specific types // For status tab search results, filter to status-specific types
if (isSearchMode && activeTab === "status") { if (isSearchMode && activeTab === "status") {
@ -926,49 +926,49 @@ export function InboxSidebar({
</TabsList> </TabsList>
</Tabs> </Tabs>
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2"> <div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{(isSearchMode ? isSearchLoading : loading) ? ( {(isSearchMode ? isSearchLoading : loading) ? (
<div className="space-y-2"> <div className="space-y-2">
{activeTab === "comments" {activeTab === "comments"
? /* Comments skeleton: avatar + two-line text + time */ ? /* Comments skeleton: avatar + two-line text + time */
[85, 60, 90, 70, 50, 75].map((titleWidth, i) => ( [85, 60, 90, 70, 50, 75].map((titleWidth, i) => (
<div <div
key={`skeleton-comment-${i}`} key={`skeleton-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]" className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
> >
<Skeleton className="h-8 w-8 rounded-full shrink-0" /> <Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2"> <div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} /> <Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" /> <Skeleton className="h-2.5 w-[70%] rounded" />
</div>
<Skeleton className="h-3 w-6 shrink-0 rounded" />
</div> </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) => (
: /* Status skeleton: status icon circle + two-line text + time */ <div
[75, 90, 55, 80, 65, 85].map((titleWidth, i) => ( key={`skeleton-status-${i}`}
<div className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
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-8 w-8 rounded-full shrink-0" /> <Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<div className="flex-1 min-w-0 space-y-2"> <Skeleton className="h-2.5 w-[60%] rounded" />
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} /> </div>
<Skeleton className="h-2.5 w-[60%] rounded" /> <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>
<div className="flex items-center gap-1.5 shrink-0"> ))}
<Skeleton className="h-3 w-6 rounded" /> </div>
<Skeleton className="h-2 w-2 rounded-full" /> ) : filteredItems.length > 0 ? (
</div> <div className="space-y-2">
</div> {filteredItems.map((item, index) => {
))} const isMarkingAsRead = markingAsReadId === item.id;
</div> // Place prefetch trigger on 5th item from end (only when not searching)
) : filteredItems.length > 0 ? ( const isPrefetchTrigger =
<div className="space-y-2"> !isSearchMode && hasMore && index === filteredItems.length - 5;
{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 ( return (
<div <div
@ -1023,54 +1023,53 @@ export function InboxSidebar({
</div> </div>
); );
})} })}
{/* Fallback trigger at the very end if less than 5 items and not searching */} {/* Fallback trigger at the very end if less than 5 items and not searching */}
{!isSearchMode && filteredItems.length < 5 && hasMore && ( {!isSearchMode && filteredItems.length < 5 && hasMore && (
<div ref={prefetchTriggerRef} className="h-1" /> <div ref={prefetchTriggerRef} className="h-1" />
)} )}
{/* Loading more skeletons at the bottom during infinite scroll */} {/* Loading more skeletons at the bottom during infinite scroll */}
{loadingMore && ( {loadingMore &&
activeTab === "comments" (activeTab === "comments"
? [80, 60, 90].map((titleWidth, i) => ( ? [80, 60, 90].map((titleWidth, i) => (
<div <div
key={`loading-more-comment-${i}`} key={`loading-more-comment-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]" className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
> >
<Skeleton className="h-8 w-8 rounded-full shrink-0" /> <Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2"> <div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} /> <Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[70%] rounded" /> <Skeleton className="h-2.5 w-[70%] rounded" />
</div> </div>
<Skeleton className="h-3 w-6 shrink-0 rounded" /> <Skeleton className="h-3 w-6 shrink-0 rounded" />
</div> </div>
)) ))
: [70, 85, 55].map((titleWidth, i) => ( : [70, 85, 55].map((titleWidth, i) => (
<div <div
key={`loading-more-status-${i}`} key={`loading-more-status-${i}`}
className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]" className="flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
> >
<Skeleton className="h-8 w-8 rounded-full shrink-0" /> <Skeleton className="h-8 w-8 rounded-full shrink-0" />
<div className="flex-1 min-w-0 space-y-2"> <div className="flex-1 min-w-0 space-y-2">
<Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} /> <Skeleton className="h-3 rounded" style={{ width: `${titleWidth}%` }} />
<Skeleton className="h-2.5 w-[60%] rounded" /> <Skeleton className="h-2.5 w-[60%] rounded" />
</div> </div>
<div className="flex items-center gap-1.5 shrink-0"> <div className="flex items-center gap-1.5 shrink-0">
<Skeleton className="h-3 w-6 rounded" /> <Skeleton className="h-3 w-6 rounded" />
<Skeleton className="h-2 w-2 rounded-full" /> <Skeleton className="h-2 w-2 rounded-full" />
</div> </div>
</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> </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"> <div className="text-center py-8">
{activeTab === "comments" ? ( {activeTab === "comments" ? (

View file

@ -1,6 +1,16 @@
"use client"; "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 { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { import {

View file

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

View file

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

View file

@ -54,8 +54,7 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const { data: globalConfigs, isLoading: globalLoading } = const { data: globalConfigs, isLoading: globalLoading } = useAtomValue(globalImageGenConfigsAtom);
useAtomValue(globalImageGenConfigsAtom);
const { data: userConfigs, isLoading: userLoading } = useAtomValue(imageGenConfigsAtom); const { data: userConfigs, isLoading: userLoading } = useAtomValue(imageGenConfigsAtom);
const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom); const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
@ -225,59 +224,59 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
<Globe className="size-3.5" /> <Globe className="size-3.5" />
Global Image Models Global Image Models
</div> </div>
{filteredGlobal.map((config) => { {filteredGlobal.map((config) => {
const isSelected = currentConfig?.id === config.id; const isSelected = currentConfig?.id === config.id;
const isAuto = "is_auto_mode" in config && config.is_auto_mode; const isAuto = "is_auto_mode" in config && config.is_auto_mode;
return ( return (
<CommandItem <CommandItem
key={`g-${config.id}`} key={`g-${config.id}`}
value={`g-${config.id}`} value={`g-${config.id}`}
onSelect={() => handleSelect(config.id)} onSelect={() => handleSelect(config.id)}
className={cn( className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80", isSelected && "bg-accent/80",
isAuto && "border border-violet-200 dark:border-violet-800/50" isAuto && "border border-violet-200 dark:border-violet-800/50"
)} )}
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<div className="shrink-0"> <div className="shrink-0">
{isAuto ? ( {isAuto ? (
<Shuffle className="size-4 text-violet-500" /> <Shuffle className="size-4 text-violet-500" />
) : ( ) : (
<ImageIcon className="size-4 text-teal-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>
<div className="min-w-0 flex-1"> </CommandItem>
<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>
);
})}
</CommandGroup> </CommandGroup>
)} )}
@ -290,51 +289,49 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe
<User className="size-3.5" /> <User className="size-3.5" />
Your Image Models Your Image Models
</div> </div>
{filteredUser.map((config) => { {filteredUser.map((config) => {
const isSelected = currentConfig?.id === config.id; const isSelected = currentConfig?.id === config.id;
return ( return (
<CommandItem <CommandItem
key={`u-${config.id}`} key={`u-${config.id}`}
value={`u-${config.id}`} value={`u-${config.id}`}
onSelect={() => handleSelect(config.id)} onSelect={() => handleSelect(config.id)}
className={cn( className={cn(
"mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50",
isSelected && "bg-accent/80" 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>
)} )}
</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> </CommandGroup>
</> </>
)} )}

View file

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

View file

@ -184,7 +184,12 @@ export function Pricing({
</div> </div>
<p className="text-xs leading-5 text-muted-foreground"> <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> </p>
<ul className="mt-5 gap-2 flex flex-col"> <ul className="mt-5 gap-2 flex flex-col">

View file

@ -95,16 +95,29 @@ const item = {
export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
// Image gen config atoms // Image gen config atoms
const { mutateAsync: createConfig, isPending: isCreating, error: createError } = const {
useAtomValue(createImageGenConfigMutationAtom); mutateAsync: createConfig,
const { mutateAsync: updateConfig, isPending: isUpdating, error: updateError } = isPending: isCreating,
useAtomValue(updateImageGenConfigMutationAtom); error: createError,
const { mutateAsync: deleteConfig, isPending: isDeleting, error: deleteError } = } = useAtomValue(createImageGenConfigMutationAtom);
useAtomValue(deleteImageGenConfigMutationAtom); const {
mutateAsync: updateConfig,
isPending: isUpdating,
error: updateError,
} = useAtomValue(updateImageGenConfigMutationAtom);
const {
mutateAsync: deleteConfig,
isPending: isDeleting,
error: deleteError,
} = useAtomValue(deleteImageGenConfigMutationAtom);
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
const { data: userConfigs, isFetching: configsLoading, error: fetchError, refetch: refreshConfigs } = const {
useAtomValue(imageGenConfigsAtom); data: userConfigs,
isFetching: configsLoading,
error: fetchError,
refetch: refreshConfigs,
} = useAtomValue(imageGenConfigsAtom);
const { data: globalConfigs = [], isFetching: globalLoading } = const { data: globalConfigs = [], isFetching: globalLoading } =
useAtomValue(globalImageGenConfigsAtom); useAtomValue(globalImageGenConfigsAtom);
const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom); const { data: preferences = {}, isFetching: prefsLoading } = useAtomValue(llmPreferencesAtom);
@ -249,7 +262,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
data: { data: {
image_generation_config_id: image_generation_config_id:
typeof selectedPrefId === "string" typeof selectedPrefId === "string"
? selectedPrefId ? parseInt(selectedPrefId) : undefined ? selectedPrefId
? parseInt(selectedPrefId)
: undefined
: selectedPrefId, : selectedPrefId,
}, },
}); });
@ -289,7 +304,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{/* Errors */} {/* Errors */}
<AnimatePresence> <AnimatePresence>
{errors.map((err) => ( {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"> <Alert variant="destructive" className="py-3">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" /> <AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">{err?.message}</AlertDescription> <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" /> <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"> <AlertDescription className="text-teal-800 dark:text-teal-200 text-xs md:text-sm">
<span className="font-medium"> <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>{" "} </span>{" "}
available from your administrator. available from your administrator.
</AlertDescription> </AlertDescription>
@ -342,18 +363,27 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</SelectItem> </SelectItem>
{globalConfigs.length > 0 && ( {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) => { {globalConfigs.map((c) => {
const isAuto = "is_auto_mode" in c && c.is_auto_mode; const isAuto = "is_auto_mode" in c && c.is_auto_mode;
return ( return (
<SelectItem key={`g-${c.id}`} value={c.id.toString()}> <SelectItem key={`g-${c.id}`} value={c.id.toString()}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isAuto ? ( {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"> <Badge
<Shuffle className="size-3 mr-1" />AUTO 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>
) : ( ) : (
<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} {c.provider}
</Badge> </Badge>
)} )}
@ -366,11 +396,15 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)} )}
{(userConfigs?.length ?? 0) > 0 && ( {(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) => ( {userConfigs?.map((c) => (
<SelectItem key={`u-${c.id}`} value={c.id.toString()}> <SelectItem key={`u-${c.id}`} value={c.id.toString()}>
<div className="flex items-center gap-2"> <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>{c.name}</span>
<span className="text-muted-foreground">({c.model_name})</span> <span className="text-muted-foreground">({c.model_name})</span>
</div> </div>
@ -382,10 +416,23 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</Select> </Select>
{hasPrefChanges && ( {hasPrefChanges && (
<div className="flex gap-2 pt-1"> <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"} {isSavingPref ? "Saving..." : "Save"}
</Button> </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 Reset
</Button> </Button>
</div> </div>
@ -409,7 +456,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<div className="space-y-4 md:space-y-6"> <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"> <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> <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" /> <Plus className="h-3 w-3 md:h-4 md:w-4" />
Add Image Model Add Image Model
</Button> </Button>
@ -435,7 +485,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<motion.div variants={container} initial="hidden" animate="show" className="grid gap-4"> <motion.div variants={container} initial="hidden" animate="show" className="grid gap-4">
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{userConfigs?.map((config) => ( {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"> <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"> <CardContent className="p-0">
<div className="flex"> <div className="flex">
@ -448,8 +503,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</div> </div>
<div className="flex-1 min-w-0 space-y-2"> <div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
<h4 className="text-sm md:text-base font-semibold truncate">{config.name}</h4> <h4 className="text-sm md:text-base font-semibold truncate">
<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.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} {config.provider}
</Badge> </Badge>
</div> </div>
@ -457,7 +517,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{config.model_name} {config.model_name}
</code> </code>
{config.description && ( {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"> <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" /> <Clock className="h-2.5 w-2.5 md:h-3 md:w-3" />
@ -469,7 +531,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <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" /> <Edit3 className="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@ -479,7 +546,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <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" /> <Trash2 className="h-3.5 w-3.5" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
@ -501,15 +573,30 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)} )}
{/* Create/Edit Dialog */} {/* 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"> <DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <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"} {editingConfig ? "Edit Image Model" : "Add Image Model"}
</DialogTitle> </DialogTitle>
<DialogDescription> <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> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -541,7 +628,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Label className="text-sm font-medium">Provider *</Label> <Label className="text-sm font-medium">Provider *</Label>
<Select <Select
value={formData.provider} value={formData.provider}
onValueChange={(val) => setFormData((p) => ({ ...p, provider: val, model_name: "" }))} onValueChange={(val) =>
setFormData((p) => ({ ...p, provider: val, model_name: "" }))
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a provider" /> <SelectValue placeholder="Select a provider" />
@ -565,7 +654,11 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{suggestedModels.length > 0 ? ( {suggestedModels.length > 0 ? (
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}> <Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
<PopoverTrigger asChild> <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..."} {formData.model_name || "Select or type a model..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@ -579,7 +672,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
/> />
<CommandList> <CommandList>
<CommandEmpty> <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> </CommandEmpty>
<CommandGroup> <CommandGroup>
{suggestedModels.map((m) => ( {suggestedModels.map((m) => (
@ -591,7 +686,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
setModelComboboxOpen(false); 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="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> </CommandItem>
@ -650,14 +750,24 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Button <Button
variant="outline" variant="outline"
className="flex-1" className="flex-1"
onClick={() => { setIsDialogOpen(false); setEditingConfig(null); resetForm(); }} onClick={() => {
setIsDialogOpen(false);
setEditingConfig(null);
resetForm();
}}
> >
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1" className="flex-1"
onClick={handleFormSubmit} 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} {isSubmitting ? <Spinner size="sm" className="mr-2" /> : null}
{editingConfig ? "Save Changes" : "Create & Use"} {editingConfig ? "Save Changes" : "Create & Use"}
@ -668,7 +778,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</Dialog> </Dialog>
{/* Delete Confirmation */} {/* Delete Confirmation */}
<AlertDialog open={!!configToDelete} onOpenChange={(open) => !open && setConfigToDelete(null)}> <AlertDialog
open={!!configToDelete}
onOpenChange={(open) => !open && setConfigToDelete(null)}
>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2"> <AlertDialogTitle className="flex items-center gap-2">
@ -676,13 +789,28 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
Delete Image Model Delete Image Model
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <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> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel> <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> <AlertDialogAction
{isDeleting ? <><Spinner size="sm" className="mr-2" />Deleting</> : <><Trash2 className="mr-2 h-4 w-4" />Delete</>} 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> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View file

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

View file

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

View file

@ -213,9 +213,7 @@ export const getImageGenConfigsResponse = z.array(imageGenerationConfig);
export const updateImageGenConfigRequest = z.object({ export const updateImageGenConfigRequest = z.object({
id: z.number(), id: z.number(),
data: imageGenerationConfig data: imageGenerationConfig.omit({ id: true, created_at: true, search_space_id: true }).partial(),
.omit({ id: true, created_at: true, search_space_id: true })
.partial(),
}); });
export const updateImageGenConfigResponse = imageGenerationConfig; export const updateImageGenConfigResponse = imageGenerationConfig;

View file

@ -32,11 +32,9 @@ class ImageGenConfigApiService {
const msg = parsed.error.issues.map((i) => i.message).join(", "); const msg = parsed.error.issues.map((i) => i.message).join(", ");
throw new ValidationError(`Invalid request: ${msg}`); throw new ValidationError(`Invalid request: ${msg}`);
} }
return baseApiService.post( return baseApiService.post(`/api/v1/image-generation-configs`, createImageGenConfigResponse, {
`/api/v1/image-generation-configs`, body: parsed.data,
createImageGenConfigResponse, });
{ body: parsed.data }
);
}; };
/** /**