This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-28 15:54:10 -08:00
commit 35904ba0c8
43 changed files with 1019 additions and 663 deletions

View file

@ -162,5 +162,52 @@ export default function BlockNoteEditor({
}, [resolvedTheme]);
// Renders the editor instance
return <BlockNoteView editor={editor} theme={blockNoteTheme} />;
return (
<div className="bn-container">
<style>{`
@media (max-width: 640px) {
.bn-container .bn-editor {
padding-inline: 12px !important;
}
/* Heading Level 1 (Title) */
.bn-container [data-content-type="heading"][data-level="1"] {
font-size: 1.75rem !important;
line-height: 1.2 !important;
margin-top: 1rem !important;
margin-bottom: 0.5rem !important;
}
/* Heading Level 2 */
.bn-container [data-content-type="heading"][data-level="2"] {
font-size: 1.5rem !important;
line-height: 1.2 !important;
margin-top: 0.875rem !important;
margin-bottom: 0.375rem !important;
}
/* Heading Level 3 */
.bn-container [data-content-type="heading"][data-level="3"] {
font-size: 1.25rem !important;
line-height: 1.2 !important;
margin-top: 0.75rem !important;
margin-bottom: 0.25rem !important;
}
/* Paragraphs and regular content */
.bn-container .bn-block-content {
font-size: 0.9375rem !important;
line-height: 1.5 !important;
}
/* Adjust lists */
.bn-container ul,
.bn-container ol {
padding-left: 1.25rem !important;
}
}
`}</style>
<BlockNoteView editor={editor} theme={blockNoteTheme} />
</div>
);
}

View file

@ -34,8 +34,8 @@ export function LanguageSwitcher() {
return (
<Select value={locale} onValueChange={handleLanguageChange}>
<SelectTrigger className="w-[160px]">
<Globe className="mr-2 h-4 w-4" />
<SelectTrigger className="w-[110px] sm:w-[160px] h-8 sm:h-10 text-xs sm:text-sm px-2 sm:px-3 gap-1 sm:gap-2">
<Globe className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
<SelectValue>
{languages.find((lang) => lang.code === locale)?.name || "English"}
</SelectValue>

View file

@ -1,6 +1,6 @@
"use client";
import { BadgeCheck, LogOut, Settings } from "lucide-react";
import { BadgeCheck, LogOut } from "lucide-react";
import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
@ -51,23 +51,28 @@ export function UserDropdown({
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<DropdownMenuContent className="w-44 md:w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal p-2 md:p-3">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
<p className="text-xs md:text-sm font-medium leading-none">{user.name}</p>
<p className="text-[10px] md:text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => router.push(`/dashboard/api-key`)}>
<BadgeCheck className="mr-2 h-4 w-4" />
<DropdownMenuItem
onClick={() => router.push(`/dashboard/api-key`)}
className="text-xs md:text-sm"
>
<BadgeCheck className="mr-2 h-3.5 w-3.5 md:h-4 md:w-4" />
API Key
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<DropdownMenuItem onClick={handleLogout} className="text-xs md:text-sm">
<LogOut className="mr-2 h-3.5 w-3.5 md:h-4 md:w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -154,8 +154,8 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
"text-muted-foreground hover:text-foreground"
)}
>
{/* Header text with shimmer if processing or has in-progress step */}
{isProcessing || inProgressStep ? (
{/* Header text with shimmer if processing (streaming) */}
{isProcessing ? (
<TextShimmerLoader text={getHeaderText()} size="sm" />
) : (
<span>{getHeaderText()}</span>
@ -398,7 +398,7 @@ const ThreadWelcome: FC = () => {
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
{/* Greeting positioned above the composer - fixed position */}
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-5xl delay-100 duration-500 ease-out fill-mode-both">
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-3xl md:text-5xl delay-100 duration-500 ease-out fill-mode-both">
{greeting}
</h1>
</div>
@ -891,14 +891,17 @@ const ThinkingStepsPart: FC = () => {
const messageId = useAssistantState(({ message }) => message?.id);
const thinkingSteps = thinkingStepsMap.get(messageId) || [];
// Check if thread is still running (for stopping the spinner when cancelled)
// Check if this specific message is currently streaming
// A message is streaming if: thread is running AND this is the last assistant message
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
const isMessageStreaming = isThreadRunning && isLastMessage;
if (thinkingSteps.length === 0) return null;
return (
<div className="mb-3">
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isThreadRunning} />
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isMessageStreaming} />
</div>
);
};

View file

@ -2,7 +2,7 @@
import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Logo } from "@/components/Logo";
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
import { useGithubStars } from "@/hooks/use-github-stars";
@ -118,89 +118,88 @@ const MobileNav = ({ navItems, isScrolled }: any) => {
const { compactFormat: githubStars, loading: loadingGithubStars } = useGithubStars();
return (
<>
<motion.div
animate={{ borderRadius: open ? "4px" : "2rem" }}
key={String(open)}
className={cn(
"mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between px-4 py-2 lg:hidden transition-all duration-300",
isScrolled
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
: "bg-transparent border border-transparent"
)}
>
<div className="flex w-full flex-row items-center justify-between">
<div className="flex flex-row items-center gap-2">
<Logo className="h-8 w-8 rounded-md" />
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
</div>
<button
type="button"
onClick={() => setOpen(!open)}
className="relative z-50 flex items-center justify-center p-2 -mr-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
aria-label={open ? "Close menu" : "Open menu"}
>
{open ? (
<IconX className="h-6 w-6 text-black dark:text-white" />
) : (
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
)}
</button>
<motion.div
animate={{ borderRadius: open ? "4px" : "2rem" }}
key={String(open)}
className={cn(
"relative mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between px-4 py-2 lg:hidden transition-all duration-300",
isScrolled
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
: "bg-transparent border border-transparent"
)}
>
<div className="flex w-full flex-row items-center justify-between">
<div className="flex flex-row items-center gap-2">
<Logo className="h-8 w-8 rounded-md" />
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
</div>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 top-16 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white/80 backdrop-blur-md border border-white/20 shadow-lg px-4 py-8 dark:bg-neutral-950/80 dark:border-neutral-800/50"
>
{navItems.map((navItem: any, idx: number) => (
<Link
key={`link=${idx}`}
href={navItem.link}
className="relative text-neutral-600 dark:text-neutral-300"
>
<motion.span className="block">{navItem.name} </motion.span>
</Link>
))}
<div className="flex w-full items-center gap-2 pt-2">
<Link
href="https://discord.gg/ejRNvftDp9"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
</Link>
<Link
href="https://github.com/MODSetter/SurfSense"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 rounded-lg px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
{loadingGithubStars ? (
<div className="w-6 h-5 dark:bg-neutral-800 animate-pulse"></div>
) : (
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">
{githubStars}
</span>
)}
</Link>
<ThemeTogglerComponent />
</div>
<Link
href="/login"
className="w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation"
>
Sign In
</Link>
</motion.div>
<button
type="button"
onClick={() => setOpen(!open)}
className="relative z-50 flex items-center justify-center p-2 -mr-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
aria-label={open ? "Close menu" : "Open menu"}
>
{open ? (
<IconX className="h-6 w-6 text-black dark:text-white" />
) : (
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
)}
</AnimatePresence>
</motion.div>
</>
</button>
</div>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="absolute inset-x-0 top-full mt-1 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-xl bg-white/90 backdrop-blur-xl border border-white/20 shadow-2xl px-4 py-6 dark:bg-neutral-950/90 dark:border-neutral-800/50"
>
{navItems.map((navItem: any, idx: number) => (
<Link
key={`link=${idx}`}
href={navItem.link}
className="relative text-neutral-600 dark:text-neutral-300"
>
<motion.span className="block">{navItem.name} </motion.span>
</Link>
))}
<div className="flex w-full items-center gap-2 pt-2">
<Link
href="https://discord.gg/ejRNvftDp9"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
</Link>
<Link
href="https://github.com/MODSetter/SurfSense"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 rounded-lg px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
>
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
{loadingGithubStars ? (
<div className="w-6 h-5 dark:bg-neutral-800 animate-pulse"></div>
) : (
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">
{githubStars}
</span>
)}
</Link>
<ThemeTogglerComponent />
</div>
<Link
href="/login"
className="w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation"
>
Sign In
</Link>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};

View file

@ -69,7 +69,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
<div className="space-y-6 p-2 sm:p-0">
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr_auto] md:gap-3 items-end">
<div className="flex flex-col space-y-1">
<Label htmlFor="param-key" className="text-sm font-medium">
<Label htmlFor="param-key" className="text-xs sm:text-sm font-medium">
Parameter Key
</Label>
<Select value={selectedKey} onValueChange={setSelectedKey}>
@ -87,7 +87,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
</div>
<div className="flex flex-col space-y-1">
<Label htmlFor="param-value" className="text-sm font-medium">
<Label htmlFor="param-value" className="text-xs sm:text-sm font-medium">
Value
</Label>
<Input
@ -100,11 +100,11 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
</div>
<Button
className="w-full md:w-auto h-10 mt-0"
className="w-full md:w-auto h-9 sm:h-10 mt-0 text-xs sm:text-sm"
onClick={handleAdd}
disabled={!selectedKey || value === ""}
>
<Plus className="w-4 h-4 mr-2" /> Add Parameter
<Plus className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" /> Add Parameter
</Button>
</div>

View file

@ -47,11 +47,13 @@ export function JsonMetadataViewer({
if (open !== undefined && onOpenChange !== undefined) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>{title} - Metadata</DialogTitle>
<DialogTitle className="text-base sm:text-lg truncate pr-6">
{title} - Metadata
</DialogTitle>
</DialogHeader>
<div className="mt-4 p-4 bg-muted/30 rounded-md">
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
<JsonView data={jsonData} style={defaultStyles} />
</div>
</DialogContent>
@ -70,11 +72,13 @@ export function JsonMetadataViewer({
</Button>
)}
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>{title} - Metadata</DialogTitle>
<DialogTitle className="text-base sm:text-lg truncate pr-6">
{title} - Metadata
</DialogTitle>
</DialogHeader>
<div className="mt-4 p-4 bg-muted/30 rounded-md">
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
<JsonView data={jsonData} style={defaultStyles} />
</div>
</DialogContent>

View file

@ -184,8 +184,8 @@ export const DocumentMentionPicker = forwardRef<
role="listbox"
tabIndex={-1}
>
{/* Document List */}
<div className="max-h-[280px] overflow-y-auto">
{/* Document List - Shows max 3 items on mobile, 5 items on desktop */}
<div className="max-h-[108px] sm:max-h-[180px] overflow-y-auto">
{actualLoading ? (
<div className="flex items-center justify-center py-4">
<div className="animate-spin h-5 w-5 border-2 border-primary border-t-transparent rounded-full" />

View file

@ -184,7 +184,7 @@ export function ModelConfigSidebar({
<Bot className="size-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-semibold">{getTitle()}</h2>
<h2 className="text-base sm:text-lg font-semibold">{getTitle()}</h2>
<div className="flex items-center gap-2 mt-0.5">
{isGlobal ? (
<Badge variant="secondary" className="gap-1 text-xs">
@ -207,9 +207,10 @@ export function ModelConfigSidebar({
variant="ghost"
size="icon"
onClick={() => onOpenChange(false)}
className="rounded-xl hover:bg-destructive/10 hover:text-destructive"
className="h-8 w-8 rounded-full"
>
<X className="size-5" />
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>

View file

@ -175,39 +175,44 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
role="combobox"
aria-expanded={open}
className={cn(
"h-9 gap-2 px-3 rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
"h-7 md:h-9 gap-1 md:gap-2 px-2 md:px-3 rounded-lg md:rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
"text-sm font-medium text-foreground",
"text-xs md:text-sm font-medium text-foreground",
"focus-visible:ring-0 focus-visible:ring-offset-0",
className
)}
>
{isLoading ? (
<>
<Loader2 className="size-4 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">Loading...</span>
<Loader2 className="size-3.5 md:size-4 animate-spin text-muted-foreground" />
<span className="text-muted-foreground hidden md:inline">Loading...</span>
<span className="text-muted-foreground md:hidden">Load...</span>
</>
) : currentConfig ? (
<>
{getProviderIcon(currentConfig.provider)}
<span className="max-w-[150px] truncate">{currentConfig.name}</span>
<Badge variant="secondary" className="ml-1 text-[10px] px-1.5 py-0 h-4 bg-muted/80">
{currentConfig.model_name.split("/").pop()?.slice(0, 15) ||
currentConfig.model_name.slice(0, 15)}
<span className="max-w-[80px] md:max-w-[150px] truncate">{currentConfig.name}</span>
<Badge
variant="secondary"
className="ml-0.5 md:ml-1 text-[9px] md:text-[10px] px-1 md:px-1.5 py-0 h-3.5 md:h-4 bg-muted/80"
>
{currentConfig.model_name.split("/").pop()?.slice(0, 10) ||
currentConfig.model_name.slice(0, 10)}
</Badge>
</>
) : (
<>
<Bot className="size-4 text-muted-foreground" />
<span className="text-muted-foreground">Select Model</span>
<Bot className="size-3.5 md:size-4 text-muted-foreground" />
<span className="text-muted-foreground hidden md:inline">Select Model</span>
<span className="text-muted-foreground md:hidden">Model</span>
</>
)}
<ChevronDown className="size-3.5 text-muted-foreground ml-1 shrink-0" />
<ChevronDown className="size-3 md:size-3.5 text-muted-foreground ml-1 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[360px] p-0 rounded-xl shadow-lg border-border/30"
className="w-[280px] md:w-[360px] p-0 rounded-lg md:rounded-xl shadow-lg border-border/30"
align="start"
sideOffset={8}
>
@ -225,17 +230,17 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</div>
)}
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
<div className="flex items-center gap-1 md:gap-2 border-b border-border/30 px-2 md:px-3 py-1.5 md:py-2">
<CommandInput
placeholder="Search models..."
value={searchQuery}
onValueChange={setSearchQuery}
className="h-8 border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
className="h-7 md:h-8 text-xs md:text-sm border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
disabled={isSwitching}
/>
</div>
<CommandList className="max-h-[400px] overflow-y-auto">
<CommandList className="max-h-[300px] md:max-h-[400px] overflow-y-auto">
<CommandEmpty className="py-8 text-center">
<div className="flex flex-col items-center gap-2">
<Bot className="size-8 text-muted-foreground/40" />

View file

@ -352,9 +352,9 @@ export function SourceDetailPanel({
size="icon"
variant="ghost"
onClick={() => onOpenChange(false)}
className="rounded-xl h-10 w-10 hover:bg-destructive/10 hover:text-destructive transition-colors"
className="h-8 w-8 rounded-full"
>
<X className="h-5 w-5" />
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>

View file

@ -170,7 +170,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
const hasError = configsError || preferencesError || globalConfigsError;
return (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
{/* Header */}
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
<div className="flex flex-wrap gap-2">
@ -179,9 +179,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
size="sm"
onClick={() => refreshConfigs()}
disabled={isLoading}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<RefreshCw className={`h-4 w-4 ${configsLoading ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-3 w-3 md:h-4 md:w-4 ${configsLoading ? "animate-spin" : ""}`}
/>
<span className="hidden sm:inline">Refresh Configs</span>
<span className="sm:hidden">Configs</span>
</Button>
@ -190,9 +192,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
size="sm"
onClick={() => refreshPreferences()}
disabled={isLoading}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<RefreshCw className={`h-4 w-4 ${preferencesLoading ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-3 w-3 md:h-4 md:w-4 ${preferencesLoading ? "animate-spin" : ""}`}
/>
<span className="hidden sm:inline">Refresh Preferences</span>
<span className="sm:hidden">Prefs</span>
</Button>
@ -201,9 +205,9 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Error Alert */}
{hasError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<Alert variant="destructive" className="py-3 md:py-4">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
{(configsError?.message ?? "Failed to load LLM configurations") ||
(preferencesError?.message ?? "Failed to load preferences") ||
(globalConfigsError?.message ?? "Failed to load global configurations")}
@ -214,10 +218,10 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Loading State */}
{isLoading && (
<Card>
<CardContent className="flex items-center justify-center py-12">
<CardContent className="flex items-center justify-center py-8 md:py-12">
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin" />
<span>
<Loader2 className="w-4 h-4 md:w-5 md:h-5 animate-spin" />
<span className="text-xs md:text-sm">
{configsLoading && preferencesLoading
? "Loading configurations and preferences..."
: configsLoading
@ -231,27 +235,27 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Info Alert */}
{!isLoading && !hasError && (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
{availableConfigs.length === 0 ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<Alert variant="destructive" className="py-3 md:py-4">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
No LLM configurations found. Please add at least one LLM provider in the Agent
Configs tab before assigning roles.
</AlertDescription>
</Alert>
) : !isAssignmentComplete ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<Alert className="py-3 md:py-4">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
Complete all role assignments to enable full functionality. Each role serves
different purposes in your workflow.
</AlertDescription>
</Alert>
) : (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
<Alert className="py-3 md:py-4">
<CheckCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
All roles are assigned and ready to use! Your LLM configuration is complete.
</AlertDescription>
</Alert>
@ -259,7 +263,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Role Assignment Cards */}
{availableConfigs.length > 0 && (
<div className="grid gap-6">
<div className="grid gap-4 md:gap-6">
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
const IconComponent = role.icon;
const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
@ -277,28 +281,34 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
<Card
className={`border-l-4 ${currentAssignment ? "border-l-primary" : "border-l-muted"} hover:shadow-md transition-shadow`}
>
<CardHeader className="pb-3">
<CardHeader className="pb-2 md:pb-3 px-3 md:px-6 pt-3 md:pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${role.color}`}>
<IconComponent className="w-5 h-5" />
<div className="flex items-center gap-2 md:gap-3">
<div className={`p-1.5 md:p-2 rounded-lg ${role.color}`}>
<IconComponent className="w-4 h-4 md:w-5 md:h-5" />
</div>
<div>
<CardTitle className="text-lg">{role.title}</CardTitle>
<CardDescription className="mt-1">{role.description}</CardDescription>
<CardTitle className="text-base md:text-lg">{role.title}</CardTitle>
<CardDescription className="mt-0.5 md:mt-1 text-xs md:text-sm">
{role.description}
</CardDescription>
</div>
</div>
{currentAssignment && <CheckCircle className="w-5 h-5 text-green-500" />}
{currentAssignment && (
<CheckCircle className="w-4 h-4 md:w-5 md:h-5 text-green-500 shrink-0" />
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Assign LLM Configuration:</Label>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label className="text-xs md:text-sm font-medium">
Assign LLM Configuration:
</Label>
<Select
value={currentAssignment?.toString() || "unassigned"}
onValueChange={(value) => handleRoleAssignment(`${key}_llm_id`, value)}
>
<SelectTrigger>
<SelectTrigger className="h-9 md:h-10 text-xs md:text-sm">
<SelectValue placeholder="Select an LLM configuration" />
</SelectTrigger>
<SelectContent>
@ -361,23 +371,25 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
</div>
{assignedConfig && (
<div className="mt-3 p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-2 text-sm flex-wrap">
<Bot className="w-4 h-4" />
<div className="mt-2 md:mt-3 p-2 md:p-3 bg-muted/50 rounded-lg">
<div className="flex items-center gap-1.5 md:gap-2 text-xs md:text-sm flex-wrap">
<Bot className="w-3 h-3 md:w-4 md:h-4 shrink-0" />
<span className="font-medium">Assigned:</span>
<Badge variant="secondary">{assignedConfig.provider}</Badge>
<Badge variant="secondary" className="text-[10px] md:text-xs">
{assignedConfig.provider}
</Badge>
<span>{assignedConfig.name}</span>
{"is_global" in assignedConfig && assignedConfig.is_global && (
<Badge variant="outline" className="text-xs">
<Badge variant="outline" className="text-[9px] md:text-xs">
🌐 Global
</Badge>
)}
</div>
<div className="text-xs text-muted-foreground mt-1">
<div className="text-[10px] md:text-xs text-muted-foreground mt-0.5 md:mt-1">
Model: {assignedConfig.model_name}
</div>
{assignedConfig.api_base && (
<div className="text-xs text-muted-foreground">
<div className="text-[10px] md:text-xs text-muted-foreground">
Base: {assignedConfig.api_base}
</div>
)}
@ -393,18 +405,22 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{/* Action Buttons */}
{hasChanges && (
<div className="flex justify-center gap-3 pt-4">
<Button onClick={handleSave} disabled={isSaving} className="flex items-center gap-2">
<Save className="w-4 h-4" />
<div className="flex justify-center gap-2 md:gap-3 pt-3 md:pt-4">
<Button
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
>
<Save className="w-3.5 h-3.5 md:w-4 md:h-4" />
{isSaving ? "Saving..." : "Save Changes"}
</Button>
<Button
variant="outline"
onClick={handleReset}
disabled={isSaving}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
>
<RotateCcw className="w-4 h-4" />
<RotateCcw className="w-3.5 h-3.5 md:w-4 md:h-4" />
Reset
</Button>
</div>

View file

@ -160,7 +160,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
LLM_PROVIDERS.find((p) => p.value === providerValue);
return (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
{/* Header */}
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div className="flex items-center space-x-2">
@ -169,9 +169,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
size="sm"
onClick={() => refreshConfigs()}
disabled={isLoading}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<RefreshCw className={cn("h-4 w-4", isLoading && "animate-spin")} />
<RefreshCw className={cn("h-3 w-3 md:h-4 md:w-4", isLoading && "animate-spin")} />
Refresh
</Button>
</div>
@ -187,9 +187,11 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{err?.message ?? "Something went wrong"}</AlertDescription>
<Alert variant="destructive" className="py-3 md:py-4">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
{err?.message ?? "Something went wrong"}
</AlertDescription>
</Alert>
</motion.div>
))}
@ -198,9 +200,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
<Alert className="border-blue-500/30 bg-blue-500/5">
<Sparkles className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-800 dark:text-blue-200">
<Alert className="border-blue-500/30 bg-blue-500/5 py-3 md:py-4">
<Sparkles className="h-3 w-3 md:h-4 md:w-4 text-blue-600 dark:text-blue-400 shrink-0" />
<AlertDescription className="text-blue-800 dark:text-blue-200 text-xs md:text-sm">
<span className="font-medium">{globalConfigs.length} global configuration(s)</span>{" "}
available from your administrator. These are pre-configured and ready to use.{" "}
<span className="text-blue-600 dark:text-blue-300">
@ -214,10 +216,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Loading State */}
{isLoading && (
<Card>
<CardContent className="flex items-center justify-center py-16">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Loading configurations...</span>
<CardContent className="flex items-center justify-center py-10 md:py-16">
<div className="flex flex-col items-center gap-2 md:gap-3">
<Loader2 className="h-6 w-6 md:h-8 md:w-8 animate-spin text-muted-foreground" />
<span className="text-xs md:text-sm text-muted-foreground">
Loading configurations...
</span>
</div>
</CardContent>
</Card>
@ -225,11 +229,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{/* Configurations List */}
{!isLoading && (
<div className="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">
<h3 className="text-xl font-semibold tracking-tight">Your Configurations</h3>
<Button onClick={openNewDialog} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<h3 className="text-lg md:text-xl font-semibold tracking-tight">Your Configurations</h3>
<Button
onClick={openNewDialog}
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<Plus className="h-3 w-3 md:h-4 md:w-4" />
Add Configuration
</Button>
</div>
@ -237,18 +244,22 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
{configs?.length === 0 ? (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<Card className="border-dashed border-2 border-muted-foreground/25">
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
<div className="rounded-full bg-gradient-to-br from-violet-500/10 to-purple-500/10 p-6 mb-6">
<Wand2 className="h-12 w-12 text-violet-600 dark:text-violet-400" />
<CardContent className="flex flex-col items-center justify-center py-10 md:py-16 text-center">
<div className="rounded-full bg-gradient-to-br from-violet-500/10 to-purple-500/10 p-4 md:p-6 mb-4 md:mb-6">
<Wand2 className="h-8 w-8 md:h-12 md:w-12 text-violet-600 dark:text-violet-400" />
</div>
<div className="space-y-2 mb-6">
<h3 className="text-xl font-semibold">No Configurations Yet</h3>
<p className="text-muted-foreground max-w-sm">
<div className="space-y-2 mb-4 md:mb-6">
<h3 className="text-lg md:text-xl font-semibold">No Configurations Yet</h3>
<p className="text-xs md:text-sm text-muted-foreground max-w-sm">
Create your first AI configuration to customize how your agent responds
</p>
</div>
<Button onClick={openNewDialog} size="lg" className="gap-2">
<Plus className="h-4 w-4" />
<Button
onClick={openNewDialog}
size="lg"
className="gap-2 text-xs md:text-sm h-9 md:h-10"
>
<Plus className="h-3 w-3 md:h-4 md:w-4" />
Create First Configuration
</Button>
</CardContent>
@ -270,25 +281,25 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<CardContent className="p-0">
<div className="flex">
{/* Left accent bar */}
<div className="w-1.5 transition-colors bg-gradient-to-b from-violet-500/50 to-purple-500/50 group-hover:from-violet-500 group-hover:to-purple-500" />
<div className="w-1 md:w-1.5 transition-colors bg-gradient-to-b from-violet-500/50 to-purple-500/50 group-hover:from-violet-500 group-hover:to-purple-500" />
<div className="flex-1 p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 p-3 md:p-5">
<div className="flex items-start justify-between gap-2 md:gap-4">
{/* Main content */}
<div className="flex items-start gap-4 flex-1 min-w-0">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500/10 to-purple-500/10 group-hover:from-violet-500/20 group-hover:to-purple-500/20 transition-colors">
<Bot className="h-6 w-6 text-violet-600 dark:text-violet-400" />
<div className="flex items-start gap-2 md:gap-4 flex-1 min-w-0">
<div className="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-lg md:rounded-xl bg-gradient-to-br from-violet-500/10 to-purple-500/10 group-hover:from-violet-500/20 group-hover:to-purple-500/20 transition-colors shrink-0">
<Bot className="h-5 w-5 md:h-6 md:w-6 text-violet-600 dark:text-violet-400" />
</div>
<div className="flex-1 min-w-0 space-y-3">
<div className="flex-1 min-w-0 space-y-2 md:space-y-3">
{/* Title row */}
<div className="flex items-center gap-2 flex-wrap">
<h4 className="text-base font-semibold tracking-tight truncate">
<div className="flex items-center gap-1.5 md:gap-2 flex-wrap">
<h4 className="text-sm md:text-base font-semibold tracking-tight truncate">
{config.name}
</h4>
<div className="flex items-center gap-1.5 flex-wrap">
<div className="flex items-center gap-1 md:gap-1.5 flex-wrap">
<Badge
variant="secondary"
className="text-[10px] font-medium px-2 py-0.5 bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/20"
className="text-[9px] md:text-[10px] font-medium px-1.5 md:px-2 py-0.5 bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/20"
>
{config.provider}
</Badge>
@ -298,9 +309,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<TooltipTrigger>
<Badge
variant="outline"
className="text-[10px] px-2 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300"
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300"
>
<MessageSquareQuote className="h-3 w-3 mr-1" />
<MessageSquareQuote className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
Citations
</Badge>
</TooltipTrigger>
@ -317,9 +328,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
<TooltipTrigger>
<Badge
variant="outline"
className="text-[10px] px-2 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300"
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300"
>
<FileText className="h-3 w-3 mr-1" />
<FileText className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
Custom
</Badge>
</TooltipTrigger>
@ -333,21 +344,21 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
</div>
{/* Model name */}
<code className="text-xs font-mono text-muted-foreground bg-muted/50 px-2 py-1 rounded-md inline-block">
<code className="text-[10px] md:text-xs font-mono text-muted-foreground bg-muted/50 px-1.5 md:px-2 py-0.5 md:py-1 rounded-md inline-block">
{config.model_name}
</code>
{/* Description if any */}
{config.description && (
<p className="text-xs text-muted-foreground line-clamp-1">
<p className="text-[10px] md:text-xs text-muted-foreground line-clamp-1">
{config.description}
</p>
)}
{/* Footer row */}
<div className="flex items-center gap-4 pt-1">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<div className="flex items-center gap-2 md:gap-4 pt-1">
<div className="flex items-center gap-1 md:gap-1.5 text-[10px] md:text-xs text-muted-foreground">
<Clock className="h-2.5 w-2.5 md:h-3 md:w-3" />
<span>
{new Date(config.created_at).toLocaleDateString()}
</span>
@ -357,7 +368,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
</div>
{/* Actions */}
<div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-0.5 md:gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@ -365,9 +376,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
variant="ghost"
size="sm"
onClick={() => openEditDialog(config)}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-foreground"
>
<Edit3 className="h-4 w-4" />
<Edit3 className="h-3.5 w-3.5 md:h-4 md:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
@ -380,9 +391,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
variant="ghost"
size="sm"
onClick={() => setConfigToDelete(config)}
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<Trash2 className="h-3.5 w-3.5 md:h-4 md:w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>

View file

@ -92,15 +92,15 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
if (loading) {
return (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-full max-w-md" />
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-32 w-full" />
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<Skeleton className="h-16 md:h-20 w-full" />
<Skeleton className="h-24 md:h-32 w-full" />
</CardContent>
</Card>
</div>
@ -108,23 +108,23 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
}
return (
<div className="space-y-6">
<div className="space-y-4 md:space-y-6">
{/* Work in Progress Notice */}
<Alert
variant="default"
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700"
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700 py-3 md:py-4"
>
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-500" />
<AlertDescription className="text-amber-800 dark:text-amber-300">
<AlertTriangle className="h-3 w-3 md:h-4 md:w-4 text-amber-600 dark:text-amber-500 shrink-0" />
<AlertDescription className="text-amber-800 dark:text-amber-300 text-xs md:text-sm">
<span className="font-semibold">Work in Progress:</span> This functionality is currently
under development and not yet connected to the backend. Your instructions will be saved
but won't affect AI behavior until the feature is fully implemented.
</AlertDescription>
</Alert>
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<Alert className="py-3 md:py-4">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
System instructions apply to all AI interactions in this search space. They guide how the
AI responds, its tone, focus areas, and behavior patterns.
</AlertDescription>
@ -132,16 +132,19 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
{/* System Instructions Card */}
<Card>
<CardHeader>
<CardTitle>Custom System Instructions</CardTitle>
<CardDescription>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">Custom System Instructions</CardTitle>
<CardDescription className="text-xs md:text-sm">
Provide specific guidelines for how you want the AI to respond. These instructions will
be applied to all answers in this search space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="custom-instructions-settings" className="text-base font-medium">
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="custom-instructions-settings"
className="text-sm md:text-base font-medium"
>
Your Instructions
</Label>
<Textarea
@ -149,11 +152,11 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
placeholder="E.g., Always provide practical examples, be concise, focus on technical details, use simple language, respond in a specific format..."
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
rows={12}
className="resize-none font-mono text-sm"
rows={10}
className="resize-none font-mono text-xs md:text-sm"
/>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">
<p className="text-[10px] md:text-xs text-muted-foreground">
{customInstructions.length} characters
</p>
{customInstructions.length > 0 && (
@ -161,7 +164,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
variant="ghost"
size="sm"
onClick={() => setCustomInstructions("")}
className="h-auto py-1 px-2 text-xs"
className="h-auto py-0.5 md:py-1 px-1.5 md:px-2 text-[10px] md:text-xs"
>
Clear
</Button>
@ -170,9 +173,9 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
</div>
{customInstructions.trim().length === 0 && (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<Alert className="py-2 md:py-3">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
No system instructions are currently set. The AI will use default behavior.
</AlertDescription>
</Alert>
@ -181,22 +184,22 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
</Card>
{/* Action Buttons */}
<div className="flex items-center justify-between pt-4">
<div className="flex items-center justify-between pt-3 md:pt-4 gap-2">
<Button
variant="outline"
onClick={handleReset}
disabled={!hasChanges || saving}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
>
<RotateCcw className="h-4 w-4" />
<RotateCcw className="h-3.5 w-3.5 md:h-4 md:w-4" />
Reset Changes
</Button>
<Button
onClick={handleSave}
disabled={!hasChanges || saving}
className="flex items-center gap-2"
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
>
<Save className="h-4 w-4" />
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
{saving ? "Saving..." : "Save Instructions"}
</Button>
</div>
@ -204,10 +207,10 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
{hasChanges && (
<Alert
variant="default"
className="bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800"
className="bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800 py-3 md:py-4"
>
<Info className="h-4 w-4 text-blue-600 dark:text-blue-500" />
<AlertDescription className="text-blue-800 dark:text-blue-300">
<Info className="h-3 w-3 md:h-4 md:w-4 text-blue-600 dark:text-blue-500 shrink-0" />
<AlertDescription className="text-blue-800 dark:text-blue-300 text-xs md:text-sm">
You have unsaved changes. Click "Save Instructions" to apply them.
</AlertDescription>
</Alert>

View file

@ -156,8 +156,8 @@ export function LLMConfigForm({
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
{/* System Instructions & Citations Section */}
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<MessageSquareQuote className="h-4 w-4" />
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
<MessageSquareQuote className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
System Instructions
</div>
@ -168,7 +168,7 @@ export function LLMConfigForm({
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Instructions for the AI</FormLabel>
<FormLabel className="text-xs sm:text-sm">Instructions for the AI</FormLabel>
{defaultInstructions && (
<Button
type="button"
@ -177,7 +177,7 @@ export function LLMConfigForm({
onClick={() =>
field.onChange(defaultInstructions.default_system_instructions)
}
className="h-7 text-xs text-muted-foreground hover:text-foreground"
className="h-7 text-[10px] sm:text-xs text-muted-foreground hover:text-foreground"
>
Reset to Default
</Button>
@ -187,11 +187,11 @@ export function LLMConfigForm({
<Textarea
placeholder="Enter system instructions for the AI..."
rows={6}
className="font-mono text-xs resize-none"
className="font-mono text-[11px] sm:text-xs resize-none"
{...field}
/>
</FormControl>
<FormDescription className="text-xs">
<FormDescription className="text-[10px] sm:text-xs">
Use {"{resolved_today}"} to include today's date dynamically
</FormDescription>
<FormMessage />
@ -206,8 +206,8 @@ export function LLMConfigForm({
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 bg-muted/30">
<div className="space-y-0.5">
<FormLabel className="text-sm font-medium">Enable Citations</FormLabel>
<FormDescription className="text-xs">
<FormLabel className="text-xs sm:text-sm font-medium">Enable Citations</FormLabel>
<FormDescription className="text-[10px] sm:text-xs">
Include [citation:id] references to source documents
</FormDescription>
</div>
@ -223,8 +223,8 @@ export function LLMConfigForm({
{/* Model Configuration Section */}
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Bot className="h-4 w-4" />
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
<Bot className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
Model Configuration
</div>
@ -235,7 +235,7 @@ export function LLMConfigForm({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
<Sparkles className="h-3.5 w-3.5 text-violet-500" />
Configuration Name
</FormLabel>
@ -256,7 +256,7 @@ export function LLMConfigForm({
name="description"
render={({ field }) => (
<FormItem>
<FormLabel className="text-muted-foreground">
<FormLabel className="text-muted-foreground text-xs sm:text-sm">
Description
<Badge variant="outline" className="ml-2 text-[10px]">
Optional
@ -277,7 +277,7 @@ export function LLMConfigForm({
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>LLM Provider</FormLabel>
<FormLabel className="text-xs sm:text-sm">LLM Provider</FormLabel>
<Select value={field.value} onValueChange={handleProviderChange}>
<FormControl>
<SelectTrigger className="transition-all focus:ring-violet-500/50">
@ -315,7 +315,7 @@ export function LLMConfigForm({
name="custom_provider"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Provider Name</FormLabel>
<FormLabel className="text-xs sm:text-sm">Custom Provider Name</FormLabel>
<FormControl>
<Input
placeholder="my-custom-provider"
@ -337,7 +337,7 @@ export function LLMConfigForm({
name="model_name"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Model Name</FormLabel>
<FormLabel className="text-xs sm:text-sm">Model Name</FormLabel>
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
<PopoverTrigger asChild>
<FormControl>
@ -410,7 +410,7 @@ export function LLMConfigForm({
</PopoverContent>
</Popover>
{selectedProvider?.example && (
<FormDescription className="text-xs">
<FormDescription className="text-[10px] sm:text-xs">
Example: {selectedProvider.example}
</FormDescription>
)}
@ -426,7 +426,7 @@ export function LLMConfigForm({
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
<Key className="h-3.5 w-3.5 text-amber-500" />
API Key
</FormLabel>
@ -438,7 +438,7 @@ export function LLMConfigForm({
/>
</FormControl>
{watchProvider === "OLLAMA" && (
<FormDescription className="text-xs">
<FormDescription className="text-[10px] sm:text-xs">
Ollama doesn't require auth enter any value
</FormDescription>
)}
@ -452,7 +452,7 @@ export function LLMConfigForm({
name="api_base"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
API Base URL
{selectedProvider?.apiBase && (
<Badge variant="secondary" className="text-[10px]">
@ -510,8 +510,8 @@ export function LLMConfigForm({
<>
<Separator />
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Sparkles className="h-4 w-4" />
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
<Sparkles className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
Advanced Parameters
</div>
@ -542,19 +542,29 @@ export function LLMConfigForm({
)}
>
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isSubmitting}
className="text-xs sm:text-sm h-9 sm:h-10"
>
Cancel
</Button>
)}
<Button type="submit" disabled={isSubmitting} className="gap-2 min-w-[160px]">
<Button
type="submit"
disabled={isSubmitting}
className="gap-2 min-w-[140px] sm:min-w-[160px] text-xs sm:text-sm h-9 sm:h-10"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 animate-spin" />
{mode === "edit" ? "Updating..." : "Creating..."}
</>
) : (
<>
{!compact && <Rocket className="h-4 w-4" />}
{!compact && <Rocket className="h-3.5 w-3.5 sm:h-4 sm:w-4" />}
{submitLabel ?? (mode === "edit" ? "Update Configuration" : "Create Configuration")}
</>
)}

View file

@ -42,9 +42,15 @@ interface AllChatsSidebarProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchSpaceId: string;
onCloseMobileSidebar?: () => void;
}
export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsSidebarProps) {
export function AllChatsSidebar({
open,
onOpenChange,
searchSpaceId,
onCloseMobileSidebar,
}: AllChatsSidebarProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const params = useParams();
@ -61,6 +67,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
const [searchQuery, setSearchQuery] = useState("");
const [showArchived, setShowArchived] = useState(false);
const [mounted, setMounted] = useState(false);
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
const isSearchMode = !!debouncedSearchQuery.trim();
@ -120,8 +127,10 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
(threadId: number) => {
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
onOpenChange(false);
// Also close the main sidebar on mobile
onCloseMobileSidebar?.();
},
[router, onOpenChange, searchSpaceId]
[router, onOpenChange, searchSpaceId, onCloseMobileSidebar]
);
// Handle thread deletion
@ -209,7 +218,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 bg-black/50"
className="fixed inset-0 z-[70] bg-black/50"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
@ -220,7 +229,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
role="dialog"
aria-modal="true"
aria-label={t("all_chats") || "All Chats"}
@ -345,14 +354,17 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
</Tooltip>
{/* Actions dropdown */}
<DropdownMenu>
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
"opacity-0 group-hover:opacity-100 focus:opacity-100",
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isBusy}
@ -365,7 +377,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuContent align="end" className="w-40 z-[80]">
<DropdownMenuItem
onClick={() => handleToggleArchive(thread.id, thread.archived)}
disabled={isArchiving}

View file

@ -27,6 +27,7 @@ interface AllNotesSidebarProps {
onOpenChange: (open: boolean) => void;
searchSpaceId: string;
onAddNote?: () => void;
onCloseMobileSidebar?: () => void;
}
export function AllNotesSidebar({
@ -34,6 +35,7 @@ export function AllNotesSidebar({
onOpenChange,
searchSpaceId,
onAddNote,
onCloseMobileSidebar,
}: AllNotesSidebarProps) {
const t = useTranslations("sidebar");
const router = useRouter();
@ -45,6 +47,7 @@ export function AllNotesSidebar({
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [mounted, setMounted] = useState(false);
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
// Handle mounting for portal
@ -114,8 +117,10 @@ export function AllNotesSidebar({
(noteId: number, noteSearchSpaceId: number) => {
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
onOpenChange(false);
// Also close the main sidebar on mobile
onCloseMobileSidebar?.();
},
[router, onOpenChange]
[router, onOpenChange, onCloseMobileSidebar]
);
// Handle note deletion
@ -195,7 +200,7 @@ export function AllNotesSidebar({
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 bg-black/50"
className="fixed inset-0 z-[70] bg-black/50"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
@ -206,7 +211,7 @@ export function AllNotesSidebar({
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
role="dialog"
aria-modal="true"
aria-label={t("all_notes") || "All Notes"}
@ -307,14 +312,17 @@ export function AllNotesSidebar({
</Tooltip>
{/* Actions dropdown - separate from main click area */}
<DropdownMenu>
<DropdownMenu
open={openDropdownId === note.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? note.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
"opacity-0 group-hover:opacity-100 focus:opacity-100",
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isDeleting}
@ -327,7 +335,7 @@ export function AllNotesSidebar({
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuContent align="end" className="w-40 z-[80]">
<DropdownMenuItem
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
className="text-destructive focus:text-destructive"

View file

@ -28,6 +28,7 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
@ -73,6 +74,7 @@ export function NavChats({
const router = useRouter();
const pathname = usePathname();
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
@ -119,7 +121,7 @@ export function NavChats({
</CollapsibleTrigger>
{/* Action buttons - always visible on hover */}
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
{searchSpaceId && chats.length > 0 && (
<Button
variant="ghost"
@ -171,7 +173,7 @@ export function NavChats({
size="icon"
className={cn(
"h-6 w-6",
"opacity-0 group-hover/chat:opacity-100 focus:opacity-100",
"md:opacity-0 md:group-hover/chat:opacity-100 md:focus:opacity-100",
"data-[state=open]:opacity-100",
"transition-opacity"
)}
@ -242,6 +244,7 @@ export function NavChats({
open={isAllChatsSidebarOpen}
onOpenChange={setIsAllChatsSidebarOpen}
searchSpaceId={searchSpaceId}
onCloseMobileSidebar={() => setOpenMobile(false)}
/>
)}
</SidebarGroup>

View file

@ -28,6 +28,7 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useLogsSummary } from "@/hooks/use-logs";
import { useIsMobile } from "@/hooks/use-mobile";
@ -75,6 +76,7 @@ export function NavNotes({
const router = useRouter();
const pathname = usePathname();
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
@ -138,7 +140,7 @@ export function NavNotes({
</CollapsibleTrigger>
{/* Action buttons - always visible on hover */}
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
{searchSpaceId && notes.length > 0 && (
<Button
variant="ghost"
@ -209,7 +211,7 @@ export function NavNotes({
size="icon"
className={cn(
"h-6 w-6",
"opacity-0 group-hover/note:opacity-100 focus:opacity-100",
"md:opacity-0 md:group-hover/note:opacity-100 md:focus:opacity-100",
"data-[state=open]:opacity-100",
"transition-opacity"
)}
@ -293,6 +295,7 @@ export function NavNotes({
onOpenChange={setIsAllNotesSidebarOpen}
searchSpaceId={searchSpaceId}
onAddNote={onAddNote}
onCloseMobileSidebar={() => setOpenMobile(false)}
/>
)}
</SidebarGroup>

View file

@ -81,7 +81,7 @@ export function ConnectorsTab({ searchSpaceId }: ConnectorsTabProps) {
className="w-full"
>
<div className="flex items-center justify-between space-x-4 p-4">
<h3 className="text-xl font-semibold">{t(category.title)}</h3>
<h3 className="text-lg sm:text-xl font-semibold">{t(category.title)}</h3>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted">
<motion.div

View file

@ -217,7 +217,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
>
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>{t("file_size_limit")}</AlertDescription>
<AlertDescription className="text-xs sm:text-sm">{t("file_size_limit")}</AlertDescription>
</Alert>
<Card className="relative overflow-hidden">
@ -249,7 +249,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
>
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="text-center">
<p className="text-lg font-medium">{t("drag_drop")}</p>
<p className="text-base sm:text-lg font-medium">{t("drag_drop")}</p>
<p className="text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
</div>
</motion.div>
@ -284,8 +284,10 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>{t("selected_files", { count: files.length })}</CardTitle>
<CardDescription>
<CardTitle className="text-lg sm:text-2xl">
{t("selected_files", { count: files.length })}
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
{t("total_size")}: {formatFileSize(getTotalFileSize())}
</CardDescription>
</div>
@ -313,7 +315,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<div className="flex items-center gap-3 flex-1 min-w-0">
<FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{file.name}</p>
<p className="text-sm sm:text-base font-medium truncate">{file.name}</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="text-xs">
{formatFileSize(file.size)}
@ -361,7 +363,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
animate={{ opacity: 1, y: 0 }}
>
<Button
className="w-full py-6 text-base font-medium"
className="w-full py-4 sm:py-6 text-sm sm:text-base font-medium"
onClick={handleUpload}
disabled={isUploading || files.length === 0}
>

View file

@ -125,17 +125,19 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CardTitle className="text-lg sm:text-2xl flex items-center gap-2">
<IconBrandYoutube className="h-5 w-5" />
{t("title")}
</CardTitle>
<CardDescription>{t("subtitle")}</CardDescription>
<CardDescription className="text-xs sm:text-sm">{t("subtitle")}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="video-input">{t("label")}</Label>
<Label htmlFor="video-input" className="text-sm sm:text-base">
{t("label")}
</Label>
<TagInput
id="video-input"
tags={videoTags}
@ -212,14 +214,17 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
<CardFooter className="flex justify-between">
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/dashboard/${searchSpaceId}/documents`)}
className="text-xs sm:text-sm"
>
{t("cancel")}
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || videoTags.length === 0}
className="relative overflow-hidden"
size="sm"
className="relative overflow-hidden text-xs sm:text-sm"
>
{isSubmitting ? (
<>

View file

@ -6,6 +6,88 @@ import { z } from "zod";
export const TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]);
export type TodoStatus = z.infer<typeof TodoStatusSchema>;
/**
* Normalize various status string formats to the canonical TodoStatus
* Handles common variations from different sources:
* - Linear: Done, In Progress, Todo, Backlog, Cancelled
* - Jira: To Do, In Progress, Done, In Review, Reopened, Testing + statusCategory
* - ClickUp: Open, In Progress, Complete, Closed, Review
* - GitHub: open, closed
* - Airtable: Any custom field values
*/
export function normalizeStatus(status: unknown): TodoStatus {
if (typeof status !== "string") return "pending";
const normalized = status
.toLowerCase()
.trim()
.replace(/[\s_-]+/g, "_");
// Completed variations
// Sources: Linear (Done), Jira (Done), ClickUp (Complete, Closed), GitHub (closed)
if (
normalized === "completed" ||
normalized === "complete" ||
normalized === "done" ||
normalized === "finished" ||
normalized === "closed" ||
normalized === "resolved" ||
normalized === "fixed" ||
normalized === "shipped" ||
normalized === "released" ||
normalized === "merged"
) {
return "completed";
}
// In progress variations
// Sources: Linear (In Progress), Jira (In Progress, In Review, Testing), ClickUp (In Progress, Review)
if (
normalized === "in_progress" ||
normalized === "inprogress" ||
normalized === "started" ||
normalized === "active" ||
normalized === "working" ||
normalized === "in_review" ||
normalized === "inreview" ||
normalized === "review" ||
normalized === "reviewing" ||
normalized === "testing" ||
normalized === "in_testing" ||
normalized === "qa" ||
normalized === "in_qa" ||
normalized === "doing" ||
normalized === "wip" ||
normalized === "work_in_progress"
) {
return "in_progress";
}
// Cancelled variations
// Sources: Linear (Cancelled), Jira (Won't Fix, Duplicate)
if (
normalized === "cancelled" ||
normalized === "canceled" ||
normalized === "dropped" ||
normalized === "won't_fix" ||
normalized === "wontfix" ||
normalized === "wont_fix" ||
normalized === "duplicate" ||
normalized === "invalid" ||
normalized === "rejected" ||
normalized === "archived" ||
normalized === "removed" ||
normalized === "obsolete"
) {
return "cancelled";
}
// Pending variations (default)
// Sources: Linear (Todo, Backlog), Jira (To Do, Reopened), ClickUp (Open), GitHub (open)
// Includes: "pending", "todo", "to_do", "backlog", "open", "new", "triage", "reopened", etc.
return "pending";
}
/**
* Single todo item in a plan
* Matches deepagents TodoListMiddleware output: { content, status }
@ -67,9 +149,7 @@ export function parseSerializablePlan(data: unknown): NormalizedPlan {
return {
id: typeof todo?.id === "string" ? todo.id : `todo-${i}`,
content: typeof todo?.content === "string" ? todo.content : "Task",
status: TodoStatusSchema.safeParse(todo?.status).success
? (todo.status as TodoStatus)
: ("pending" as const),
status: normalizeStatus(todo?.status),
};
})
: [{ id: "1", content: "No tasks", status: "pending" as const }],

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className
)}
{...props}