mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
chore: ran all linting
This commit is contained in:
parent
2470fb70a6
commit
76e7ddee2f
22 changed files with 638 additions and 433 deletions
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,10 @@ export const UserMessage: FC = () => {
|
||||||
|
|
||||||
const UserActionBar: FC = () => {
|
const UserActionBar: FC = () => {
|
||||||
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
||||||
|
|
||||||
// Get current message ID
|
// Get current message ID
|
||||||
const currentMessageId = useAssistantState(({ message }) => message?.id);
|
const currentMessageId = useAssistantState(({ message }) => message?.id);
|
||||||
|
|
||||||
// Find the last user message ID in the thread (computed once, memoized by selector)
|
// Find the last user message ID in the thread (computed once, memoized by selector)
|
||||||
const lastUserMessageId = useAssistantState(({ thread }) => {
|
const lastUserMessageId = useAssistantState(({ thread }) => {
|
||||||
const messages = thread.messages;
|
const messages = thread.messages;
|
||||||
|
|
@ -117,7 +117,7 @@ const UserActionBar: FC = () => {
|
||||||
|
|
||||||
// Simple comparison - no iteration needed per message
|
// Simple comparison - no iteration needed per message
|
||||||
const isLastUserMessage = currentMessageId === lastUserMessageId;
|
const isLastUserMessage = currentMessageId === lastUserMessageId;
|
||||||
|
|
||||||
// Show edit button only on the last user message and when thread is not running
|
// Show edit button only on the last user message and when thread is not running
|
||||||
const canEdit = isLastUserMessage && !isThreadRunning;
|
const canEdit = isLastUserMessage && !isThreadRunning;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" ? (
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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" />}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue