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

@ -110,19 +110,46 @@ You have access to the following tools:
* Prioritize showing: diagrams, charts, infographics, key illustrations, or images that help explain the content. * Prioritize showing: diagrams, charts, infographics, key illustrations, or images that help explain the content.
* Don't show every image - just the most relevant 1-3 images that enhance understanding. * Don't show every image - just the most relevant 1-3 images that enhance understanding.
6. write_todos: Create and update a planning/todo list to break down complex tasks. 6. write_todos: Create and update a planning/todo list.
- IMPORTANT: Use this tool when the user asks you to create a plan, break down a task, or explain something in structured steps.
- This tool creates a visual plan with progress tracking that the user can see in the UI.
- When to use:
* User asks to "create a plan" or "break down" a task
* User asks for "steps" to do something
* User asks you to "explain" something in sections
* Any multi-step task that would benefit from structured planning
- Args: - Args:
- todos: List of todo items, each with: - todos: List of todo items, each with:
* content: Description of the task (required) * content: Description of the task (required)
* status: "pending", "in_progress", or "completed" (required) * status: "pending", "in_progress", "completed", or "cancelled" (required)
- The tool automatically adds IDs and formats the output for the UI.
STRICT MODE SELECTION - CHOOSE ONE:
[MODE A] AGENT PLAN (you will work through it)
Use when: User asks you to explain, teach, plan, or break down a concept.
Examples: "Explain how to set up Python", "Plan my trip", "Break down machine learning"
Rules:
- Create plan with first item "in_progress", rest "pending"
- After explaining each step, call write_todos again to update progress
- Only ONE item "in_progress" at a time
- Mark items "completed" as you finish explaining them
- Final call: all items "completed"
[MODE B] EXTERNAL TASK DISPLAY (from connectors - you CANNOT complete these)
Use when: User asks to show/list/display tasks from Linear, Jira, ClickUp, GitHub, Airtable, Notion, or any connector.
Examples: "Show my Linear tasks", "List Jira tickets", "Create todos from ClickUp", "Show GitHub issues"
STRICT RULES:
1. You CANNOT complete these tasks - only the user can in the actual tool
2. PRESERVE original status from source - DO NOT use agent workflow
3. Call write_todos ONCE with all tasks and their REAL statuses
4. Provide insights/summary as TEXT after the todo list, NOT as todo items
5. NO INTERNAL REASONING - Never expose your process. Do NOT say "Let me map...", "Converting statuses...", "Here's how I'll organize...", or explain mapping logic. Just call write_todos silently and provide insights.
STATUS MAPPING (apply strictly):
- "completed" Done, Completed, Complete, Closed, Resolved, Fixed, Merged, Shipped, Released
- "in_progress" In Progress, In Review, Testing, QA, Active, Doing, Started, Review, Working
- "pending" Todo, To Do, Backlog, Open, New, Pending, Triage, Reopened, Unstarted
- "cancelled" Cancelled, Canceled, Won't Fix, Duplicate, Invalid, Rejected, Archived, Obsolete
CONNECTOR-SPECIFIC:
- Linear: state.name = "Done", "In Progress", "Todo", "Backlog", "Cancelled"
- Jira: statusCategory.name = "To Do", "In Progress", "Done"
- ClickUp: status = "complete", "in progress", "open", "closed"
- GitHub: state = "open", "closed"; PRs also "merged"
- Airtable/Notion: Check field values, apply mapping above
</tools> </tools>
<tool_call_examples> <tool_call_examples>
- User: "Fetch all my notes and what's in them?" - User: "Fetch all my notes and what's in them?"
@ -181,6 +208,8 @@ You have access to the following tools:
- Call: `display_image(src="https://example.com/nn-diagram.png", alt="Neural Network Diagram", title="Neural Network Architecture")` - Call: `display_image(src="https://example.com/nn-diagram.png", alt="Neural Network Diagram", title="Neural Network Architecture")`
- Then provide your explanation, referencing the displayed image - Then provide your explanation, referencing the displayed image
[MODE A EXAMPLES] Agent Plan - you work through it:
- User: "Create a plan for building a user authentication system" - User: "Create a plan for building a user authentication system"
- Call: `write_todos(todos=[{"content": "Design database schema for users and sessions", "status": "in_progress"}, {"content": "Implement registration and login endpoints", "status": "pending"}, {"content": "Add password reset functionality", "status": "pending"}])` - Call: `write_todos(todos=[{"content": "Design database schema for users and sessions", "status": "in_progress"}, {"content": "Implement registration and login endpoints", "status": "pending"}, {"content": "Add password reset functionality", "status": "pending"}])`
- Then explain each step in detail as you work through them - Then explain each step in detail as you work through them
@ -193,21 +222,55 @@ You have access to the following tools:
- Call: `write_todos(todos=[{"content": "Research best time to visit and book flights", "status": "in_progress"}, {"content": "Plan itinerary for cities to visit", "status": "pending"}, {"content": "Book accommodations", "status": "pending"}, {"content": "Prepare travel documents and currency", "status": "pending"}])` - Call: `write_todos(todos=[{"content": "Research best time to visit and book flights", "status": "in_progress"}, {"content": "Plan itinerary for cities to visit", "status": "pending"}, {"content": "Book accommodations", "status": "pending"}, {"content": "Prepare travel documents and currency", "status": "pending"}])`
- Then provide travel preparation guidance - Then provide travel preparation guidance
- User: "Break down how to learn guitar" - COMPLETE WORKFLOW EXAMPLE - User: "Explain how to set up a Python project"
- Call: `write_todos(todos=[{"content": "Learn basic chords and finger positioning", "status": "in_progress"}, {"content": "Practice strumming patterns", "status": "pending"}, {"content": "Learn to read tabs and sheet music", "status": "pending"}, {"content": "Master simple songs", "status": "pending"}])` - STEP 1 (Create initial plan):
- Then provide learning milestones and tips Call: `write_todos(todos=[{"content": "Set up virtual environment", "status": "in_progress"}, {"content": "Create project structure", "status": "pending"}, {"content": "Configure dependencies", "status": "pending"}])`
Then explain virtual environment setup in detail...
- STEP 2 (After explaining virtual environments, update progress):
Call: `write_todos(todos=[{"content": "Set up virtual environment", "status": "completed"}, {"content": "Create project structure", "status": "in_progress"}, {"content": "Configure dependencies", "status": "pending"}])`
Then explain project structure in detail...
- STEP 3 (After explaining project structure, update progress):
Call: `write_todos(todos=[{"content": "Set up virtual environment", "status": "completed"}, {"content": "Create project structure", "status": "completed"}, {"content": "Configure dependencies", "status": "in_progress"}])`
Then explain dependency configuration in detail...
- STEP 4 (After completing all explanations, mark all done):
Call: `write_todos(todos=[{"content": "Set up virtual environment", "status": "completed"}, {"content": "Create project structure", "status": "completed"}, {"content": "Configure dependencies", "status": "completed"}])`
Provide final summary
- User: "Plan my workout routine for the week" [MODE B EXAMPLES] External Tasks - preserve original status, you CANNOT complete:
- Call: `write_todos(todos=[{"content": "Monday: Upper body strength training", "status": "in_progress"}, {"content": "Tuesday: Cardio and core workout", "status": "pending"}, {"content": "Wednesday: Rest or light stretching", "status": "pending"}, {"content": "Thursday: Lower body strength training", "status": "pending"}, {"content": "Friday: Full body HIIT session", "status": "pending"}])`
- Then provide exercise details and tips
- User: "Help me organize my home renovation project" - User: "Show my Linear tasks" or "Create todos for Linear tasks"
- Call: `write_todos(todos=[{"content": "Define scope and create budget", "status": "in_progress"}, {"content": "Research and hire contractors", "status": "pending"}, {"content": "Obtain necessary permits", "status": "pending"}, {"content": "Order materials and fixtures", "status": "pending"}, {"content": "Execute renovation phases", "status": "pending"}])` - First search: `search_knowledge_base(query="Linear tasks issues", connectors_to_search=["LINEAR_CONNECTOR"])`
- Then provide detailed renovation guidance - Then call write_todos ONCE with ORIGINAL statuses preserved:
Call: `write_todos(todos=[
{"content": "SUR-21: Add refresh button in manage documents page", "status": "completed"},
{"content": "SUR-22: Logs page not accessible in docker", "status": "completed"},
{"content": "SUR-27: Add Google Drive connector", "status": "in_progress"},
{"content": "SUR-28: Logs page should show all logs", "status": "pending"}
])`
- Then provide INSIGHTS as text (NOT as todos):
"You have 2 completed, 1 in progress, and 1 pending task. SUR-27 (Google Drive connector) is currently active. Consider prioritizing SUR-28 next."
- User: "What steps should I take to start a podcast?" - User: "List my Jira tickets"
- Call: `write_todos(todos=[{"content": "Define podcast concept and target audience", "status": "in_progress"}, {"content": "Set up recording equipment and software", "status": "pending"}, {"content": "Plan episode structure and content", "status": "pending"}, {"content": "Record and edit first episodes", "status": "pending"}, {"content": "Choose hosting platform and publish", "status": "pending"}])` - First search: `search_knowledge_base(query="Jira tickets issues", connectors_to_search=["JIRA_CONNECTOR"])`
- Then provide podcast launch guidance - Map Jira statuses: "Done" completed, "In Progress"/"In Review" in_progress, "To Do" pending
- Call write_todos ONCE with mapped statuses
- Provide summary as text after
- User: "Show ClickUp tasks"
- First search: `search_knowledge_base(query="ClickUp tasks", connectors_to_search=["CLICKUP_CONNECTOR"])`
- Map: "complete"/"closed" completed, "in progress" in_progress, "open" pending
- Call write_todos ONCE, then provide insights as text
- User: "Show my GitHub issues"
- First search: `search_knowledge_base(query="GitHub issues", connectors_to_search=["GITHUB_CONNECTOR"])`
- Map: "closed"/"merged" completed, "open" pending
- Call write_todos ONCE, then summarize as text
CRITICAL FOR MODE B:
- NEVER use the "first item in_progress, rest pending" pattern for external tasks
- NEVER pretend you will complete external tasks - be honest that only the user can
- ALWAYS preserve the actual status from the source system
- ALWAYS provide insights/summaries as regular text, not as todo items
</tool_call_examples> </tool_call_examples>
""" """

View file

@ -40,8 +40,8 @@ export function GoogleLoginButton() {
return ( return (
<div className="relative w-full overflow-hidden"> <div className="relative w-full overflow-hidden">
<AmbientBackground /> <AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center"> <div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
<Logo className="rounded-full my-8" /> <Logo className="h-16 w-16 md:h-32 md:w-32 rounded-full my-4 md:my-8 transition-all" />
{/* <h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl"> {/* <h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
Login Login
</h1> */} </h1> */}
@ -93,7 +93,7 @@ export function GoogleLoginButton() {
<motion.button <motion.button
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
className="group/btn relative flex w-full items-center justify-center space-x-2 rounded-lg bg-white px-6 py-4 text-neutral-700 shadow-lg transition-all duration-200 hover:shadow-xl dark:bg-neutral-800 dark:text-neutral-200" className="group/btn relative flex w-full items-center justify-center space-x-2 rounded-lg bg-white px-6 py-3 md:py-4 text-neutral-700 shadow-lg transition-all duration-200 hover:shadow-xl dark:bg-neutral-800 dark:text-neutral-200"
onClick={handleGoogleLogin} onClick={handleGoogleLogin}
> >
<div className="absolute inset-0 h-full w-full transform opacity-0 transition duration-200 group-hover/btn:opacity-100"> <div className="absolute inset-0 h-full w-full transform opacity-0 transition duration-200 group-hover/btn:opacity-100">

View file

@ -118,8 +118,8 @@ export function LocalLoginForm() {
}; };
return ( return (
<div className="w-full max-w-md"> <div className="w-full max-w-md px-6 md:px-0">
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-3 md:space-y-4">
{/* Error Display */} {/* Error Display */}
<AnimatePresence> <AnimatePresence>
{error && error.title && ( {error && error.title && (
@ -194,7 +194,7 @@ export function LocalLoginForm() {
required required
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -217,7 +217,7 @@ export function LocalLoginForm() {
required required
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ className={`mt-1 block w-full rounded-md border pr-10 px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -238,7 +238,7 @@ export function LocalLoginForm() {
<button <button
type="submit" type="submit"
disabled={isLoggingIn} disabled={isLoggingIn}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors" className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base"
> >
{isLoggingIn ? tCommon("loading") : t("sign_in")} {isLoggingIn ? tCommon("loading") : t("sign_in")}
</button> </button>

View file

@ -85,7 +85,7 @@ function LoginContent() {
// Get the auth type from environment variables // Get the auth type from environment variables
setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"); setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE");
setIsLoading(false); setIsLoading(false);
}, [searchParams]); }, [searchParams, t, tCommon]);
// Show loading state while determining auth type // Show loading state while determining auth type
if (isLoading) { if (isLoading) {
@ -111,8 +111,8 @@ function LoginContent() {
<div className="relative w-full overflow-hidden"> <div className="relative w-full overflow-hidden">
<AmbientBackground /> <AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center"> <div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" /> <Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl"> <h1 className="mt-4 mb-6 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:mt-8 md:mb-8 md:text-3xl lg:text-4xl transition-all">
{t("sign_in")} {t("sign_in")}
</h1> </h1>

View file

@ -157,17 +157,17 @@ export default function RegisterPage() {
return ( return (
<div className="relative w-full overflow-hidden"> <div className="relative w-full overflow-hidden">
<AmbientBackground /> <AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center"> <div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
<Logo className="rounded-md" /> <Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl"> <h1 className="mt-4 mb-6 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:mt-8 md:mb-8 md:text-3xl lg:text-4xl transition-all">
{t("create_account")} {t("create_account")}
</h1> </h1>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-3 md:space-y-4">
{/* Enhanced Error Display */} {/* Enhanced Error Display */}
<AnimatePresence> <AnimatePresence>
{error && error.title && ( {error?.title && (
<motion.div <motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }} initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
@ -239,7 +239,7 @@ export default function RegisterPage() {
required required
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -261,7 +261,7 @@ export default function RegisterPage() {
required required
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -283,7 +283,7 @@ export default function RegisterPage() {
required required
value={confirmPassword} value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -295,7 +295,7 @@ export default function RegisterPage() {
<button <button
type="submit" type="submit"
disabled={isRegistering} disabled={isRegistering}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors" className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base"
> >
{isRegistering ? t("creating_account_btn") : t("register")} {isRegistering ? t("creating_account_btn") : t("register")}
</button> </button>

View file

@ -241,7 +241,7 @@ export function DashboardClientLayout({
return ( return (
<SidebarProvider <SidebarProvider
className="h-full bg-red-600 overflow-hidden" className="h-full overflow-hidden"
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
@ -257,8 +257,10 @@ export function DashboardClientLayout({
<div className="flex items-center justify-between w-full gap-2 px-4"> <div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="h-6" /> <div className="hidden md:flex items-center gap-2">
<DashboardBreadcrumb /> <Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LanguageSwitcher /> <LanguageSwitcher />

View file

@ -278,14 +278,17 @@ export default function ConnectorsPage() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="mb-8 flex items-center justify-between" className="mb-8 flex items-center justify-between gap-2"
> >
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1> <h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-muted-foreground mt-2">{t("subtitle")}</p> <p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
</div> </div>
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}> <Button
<Plus className="mr-2 h-4 w-4" /> className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("add_connector")} {t("add_connector")}
</Button> </Button>
</motion.div> </motion.div>

View file

@ -75,14 +75,14 @@ export function DocumentsFilters({
return ( return (
<motion.div <motion.div
className="flex flex-wrap items-center justify-between gap-3" className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }} transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
<motion.div <motion.div
className="relative" className="relative w-full sm:w-auto"
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }} transition={{ type: "spring", stiffness: 300, damping: 30 }}
@ -90,7 +90,7 @@ export function DocumentsFilters({
<Input <Input
id={`${id}-input`} id={`${id}-input`}
ref={inputRef} ref={inputRef}
className="peer min-w-60 ps-9" className="peer w-full sm:min-w-60 ps-9"
value={searchValue} value={searchValue}
onChange={(e) => onSearch(e.target.value)} onChange={(e) => onSearch(e.target.value)}
placeholder={t("filter_placeholder")} placeholder={t("filter_placeholder")}
@ -231,11 +231,11 @@ export function DocumentsFilters({
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{selectedIds.size > 0 && ( {selectedIds.size > 0 && (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button className="ml-auto" variant="outline"> <Button className="w-full sm:w-auto" variant="outline">
<Trash <Trash
className="-ms-1 me-2 opacity-60" className="-ms-1 me-2 opacity-60"
size={16} size={16}

View file

@ -83,8 +83,14 @@ export function DocumentsTableShell({
const toggleAll = (checked: boolean) => { const toggleAll = (checked: boolean) => {
const next = new Set(selectedIds); const next = new Set(selectedIds);
if (checked) sorted.forEach((d) => next.add(d.id)); if (checked)
else sorted.forEach((d) => next.delete(d.id)); sorted.forEach((d) => {
next.add(d.id);
});
else
sorted.forEach((d) => {
next.delete(d.id);
});
setSelectedIds(next); setSelectedIds(next);
}; };
@ -323,26 +329,16 @@ export function DocumentsTableShell({
const icon = getDocumentTypeIcon(doc.document_type); const icon = getDocumentTypeIcon(doc.document_type);
return ( return (
<div key={doc.id} className="p-3"> <div key={doc.id} className="p-3">
<div className="flex items-start gap-3"> <div className="flex items-center gap-3">
<Checkbox <Checkbox
checked={selectedIds.has(doc.id)} checked={selectedIds.has(doc.id)}
onCheckedChange={(v) => toggleOne(doc.id, !!v)} onCheckedChange={(v) => toggleOne(doc.id, !!v)}
aria-label="Select row" aria-label="Select row"
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center gap-2 min-w-0">
<div className="flex items-center gap-2 min-w-0"> <span className="text-muted-foreground shrink-0">{icon}</span>
<span className="text-muted-foreground shrink-0">{icon}</span> <div className="font-medium truncate">{doc.title}</div>
<div className="font-medium truncate">{doc.title}</div>
</div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
</div> </div>
<div className="mt-1 flex flex-wrap items-center gap-2"> <div className="mt-1 flex flex-wrap items-center gap-2">
<DocumentTypeChip type={doc.document_type} /> <DocumentTypeChip type={doc.document_type} />
@ -371,6 +367,14 @@ export function DocumentsTableShell({
</div> </div>
)} )}
</div> </div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
</div> </div>
</div> </div>
); );

View file

@ -6,13 +6,14 @@ import { useTranslations } from "next-intl";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface ProcessingIndicatorProps { interface ProcessingIndicatorProps {
activeTasksCount: number; documentProcessorTasksCount: number;
} }
export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorProps) { export function ProcessingIndicator({ documentProcessorTasksCount }: ProcessingIndicatorProps) {
const t = useTranslations("documents"); const t = useTranslations("documents");
if (activeTasksCount === 0) return null; // Only show when there are document_processor tasks (uploads), not connector_indexing_task (periodic reindexing)
if (documentProcessorTasksCount === 0) return null;
return ( return (
<AnimatePresence> <AnimatePresence>
@ -32,7 +33,7 @@ export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorPro
{t("processing_documents")} {t("processing_documents")}
</AlertTitle> </AlertTitle>
<AlertDescription className="text-muted-foreground"> <AlertDescription className="text-muted-foreground">
{t("active_tasks_count", { count: activeTasksCount })} {t("active_tasks_count", { count: documentProcessorTasksCount })}
</AlertDescription> </AlertDescription>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { FileText, Pencil, Trash2 } from "lucide-react"; import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@ -16,6 +16,12 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { Document } from "./types"; import type { Document } from "./types";
@ -57,53 +63,108 @@ export function RowActions({
return ( return (
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
{/* Edit Button */} {/* Desktop Actions */}
<Tooltip> <div className="hidden md:flex items-center gap-1">
<TooltipTrigger asChild> <Tooltip>
<motion.div <TooltipTrigger asChild>
whileHover={{ scale: 1.1 }} <motion.div
whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.1 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }} whileTap={{ scale: 0.95 }}
> transition={{ type: "spring", stiffness: 400, damping: 17 }}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={handleEdit}
> >
<Pencil className="h-4 w-4" /> <Button
<span className="sr-only">Edit Document</span> variant="ghost"
</Button> size="icon"
</motion.div> className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
</TooltipTrigger> onClick={handleEdit}
<TooltipContent side="top"> >
<p>Edit Document</p> <Pencil className="h-4 w-4" />
</TooltipContent> <span className="sr-only">Edit Document</span>
</Tooltip> </Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit Document</p>
</TooltipContent>
</Tooltip>
{/* View Metadata Button */} <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <motion.div
<motion.div whileHover={{ scale: 1.1 }}
whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}
whileTap={{ scale: 0.95 }} transition={{ type: "spring", stiffness: 400, damping: 17 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
> >
<FileText className="h-4 w-4" /> <Button
<span className="sr-only">View Metadata</span> variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
>
<FileText className="h-4 w-4" />
<span className="sr-only">View Metadata</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>View Metadata</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
</div>
{/* Mobile Actions Dropdown */}
<div className="flex md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button> </Button>
</motion.div> </DropdownMenuTrigger>
</TooltipTrigger> <DropdownMenuContent align="end" className="w-40">
<TooltipContent side="top"> <DropdownMenuItem onClick={handleEdit}>
<p>View Metadata</p> <Pencil className="mr-2 h-4 w-4" />
</TooltipContent> <span>Edit</span>
</Tooltip> </DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsMetadataOpen(true)}>
<FileText className="mr-2 h-4 w-4" />
<span>Metadata</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setIsDeleteOpen(true)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<JsonMetadataViewer <JsonMetadataViewer
title={document.title} title={document.title}
metadata={document.document_metadata} metadata={document.document_metadata}
@ -111,30 +172,6 @@ export function RowActions({
onOpenChange={setIsMetadataOpen} onOpenChange={setIsMetadataOpen}
/> />
{/* Delete Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}> <AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View file

@ -139,6 +139,14 @@ export default function DocumentsTable() {
enablePolling: true, enablePolling: true,
refetchInterval: 5000, // Poll every 5 seconds when tasks are active refetchInterval: 5000, // Poll every 5 seconds when tasks are active
}); });
// Filter active tasks to only include document_processor tasks (uploads via "add sources")
// Exclude connector_indexing_task tasks (periodic reindexing)
const documentProcessorTasks =
summary?.active_tasks.filter((task) => task.source === "document_processor") || [];
const documentProcessorTasksCount = documentProcessorTasks.length;
const activeTasksCount = summary?.active_tasks.length || 0; const activeTasksCount = summary?.active_tasks.length || 0;
const prevActiveTasksCount = useRef(activeTasksCount); const prevActiveTasksCount = useRef(activeTasksCount);
@ -220,8 +228,8 @@ export default function DocumentsTable() {
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
> >
<div> <div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2> <h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p> <p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div> </div>
<Button onClick={refreshCurrentView} variant="outline" size="sm"> <Button onClick={refreshCurrentView} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />
@ -229,7 +237,7 @@ export default function DocumentsTable() {
</Button> </Button>
</motion.div> </motion.div>
<ProcessingIndicator activeTasksCount={activeTasksCount} /> <ProcessingIndicator documentProcessorTasksCount={documentProcessorTasksCount} />
<DocumentsFilters <DocumentsFilters
typeCounts={typeCounts ?? {}} typeCounts={typeCounts ?? {}}

View file

@ -21,7 +21,6 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { notesApiService } from "@/lib/apis/notes-api.service"; import { notesApiService } from "@/lib/apis/notes-api.service";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils"; import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
@ -130,7 +129,7 @@ export default function EditorPage() {
setError(null); setError(null);
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
setLoading(true); setLoading(true);
}, [documentId]); }, []);
// Fetch document content - DIRECT CALL TO FASTAPI // Fetch document content - DIRECT CALL TO FASTAPI
// Skip fetching if this is a new note // Skip fetching if this is a new note
@ -427,30 +426,43 @@ export default function EditorPage() {
className="flex flex-col min-h-screen w-full" className="flex flex-col min-h-screen w-full"
> >
{/* Toolbar */} {/* Toolbar */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-6"> <div className="sticky top-0 z-40 flex h-14 md:h-16 shrink-0 items-center gap-2 md:gap-4 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-3 md:px-6">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
<FileText className="h-5 w-5 text-muted-foreground shrink-0" /> <FileText className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold truncate">{displayTitle}</h1> <h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1>
{hasUnsavedChanges && <p className="text-xs text-muted-foreground">Unsaved changes</p>} {hasUnsavedChanges && (
<p className="text-[10px] md:text-xs text-muted-foreground">Unsaved changes</p>
)}
</div> </div>
</div> </div>
<Separator orientation="vertical" className="h-6" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={handleBack} disabled={saving} className="gap-2"> <Button
<ArrowLeft className="h-4 w-4" /> variant="outline"
Back onClick={handleBack}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
<ArrowLeft className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">Back</span>
</Button> </Button>
<Button onClick={handleSave} disabled={saving} className="gap-2"> <Button
onClick={handleSave}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
{saving ? ( {saving ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 md:h-4 md:w-4 animate-spin" />
{isNewNote ? "Creating..." : "Saving..."} <span className="text-xs md:text-sm">
{isNewNote ? "Creating..." : "Saving..."}
</span>
</> </>
) : ( ) : (
<> <>
<Save className="h-4 w-4" /> <Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
Save <span className="text-xs md:text-sm">Save</span>
</> </>
)} )}
</Button> </Button>
@ -459,7 +471,7 @@ export default function EditorPage() {
{/* Editor Container */} {/* Editor Container */}
<div className="flex-1 min-h-0 overflow-hidden relative"> <div className="flex-1 min-h-0 overflow-hidden relative">
<div className="h-full w-full overflow-auto p-6"> <div className="h-full w-full overflow-auto p-3 md:p-6">
{error && ( {error && (
<motion.div <motion.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}

View file

@ -506,8 +506,8 @@ export default function LogsManagePage() {
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
> >
<div> <div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2> <h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p> <p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div> </div>
<Button onClick={handleRefresh} variant="outline" size="sm"> <Button onClick={handleRefresh} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />
@ -521,48 +521,10 @@ export default function LogsManagePage() {
uniqueLevels={uniqueLevels} uniqueLevels={uniqueLevels}
uniqueStatuses={uniqueStatuses} uniqueStatuses={uniqueStatuses}
inputRef={inputRef} inputRef={inputRef}
onBulkDelete={handleDeleteRows}
id={id} id={id}
/> />
{/* Delete Button */}
{table.getSelectedRowModel().rows.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex justify-end"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteRows}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</motion.div>
)}
{/* Logs Table */} {/* Logs Table */}
<LogsTable <LogsTable
table={table} table={table}
@ -713,29 +675,31 @@ function LogsFilters({
uniqueLevels, uniqueLevels,
uniqueStatuses, uniqueStatuses,
inputRef, inputRef,
onBulkDelete,
id, id,
}: { }: {
table: any; table: any;
uniqueLevels: string[]; uniqueLevels: string[];
uniqueStatuses: string[]; uniqueStatuses: string[];
inputRef: React.RefObject<HTMLInputElement | null>; inputRef: React.RefObject<HTMLInputElement | null>;
onBulkDelete: () => Promise<void>;
id: string; id: string;
}) { }) {
const t = useTranslations("logs"); const t = useTranslations("logs");
return ( return (
<motion.div <motion.div
className="flex flex-wrap items-center justify-between gap-3" className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
{/* Search Input */} {/* Search Input */}
<motion.div className="relative" variants={fadeInScale}> <motion.div className="relative w-full sm:w-auto" variants={fadeInScale}>
<Input <Input
ref={inputRef} ref={inputRef}
className={cn( className={cn(
"peer min-w-60 ps-9", "peer w-full sm:min-w-60 ps-9",
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9" Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
)} )}
value={(table.getColumn("message")?.getFilterValue() ?? "") as string} value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
@ -806,6 +770,39 @@ function LogsFilters({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{table.getSelectedRowModel().rows.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full sm:w-auto" variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={onBulkDelete}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</motion.div> </motion.div>
); );
} }
@ -973,6 +970,7 @@ function LogsTable({
style={{ width: `${header.getSize()}px` }} style={{ width: `${header.getSize()}px` }}
className={cn( className={cn(
"h-12 px-4 py-3", "h-12 px-4 py-3",
header.column.id === "select" ? "ps-4 pe-0" : "",
// keep Created At header from wrapping and align it // keep Created At header from wrapping and align it
header.column.id === "created_at" ? "whitespace-nowrap text-right" : "" header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
)} )}
@ -1030,7 +1028,8 @@ function LogsTable({
<TableCell <TableCell
key={cell.id} key={cell.id}
className={cn( className={cn(
"px-4 py-3 align-middle overflow-hidden", "px-4 py-3 align-middle",
cell.column.id === "select" ? "ps-4 pe-0" : "overflow-hidden",
isCreatedAt isCreatedAt
? "whitespace-nowrap text-xs text-muted-foreground text-right" ? "whitespace-nowrap text-xs text-muted-foreground text-right"
: "", : "",

View file

@ -87,14 +87,14 @@ function SettingsSidebar({
<aside <aside
className={cn( className={cn(
"fixed md:relative left-0 top-0 z-50 md:z-auto", "fixed md:relative left-0 top-0 z-50 md:z-auto",
"w-72 shrink-0 border-r border-border bg-background md:bg-muted/20 h-full flex flex-col", "w-72 shrink-0 bg-background md:bg-muted/20 h-full flex flex-col",
"transition-transform duration-300 ease-out", "transition-transform duration-300 ease-out",
"md:translate-x-0", "md:translate-x-0",
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0" isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
)} )}
> >
{/* Header with back button */} {/* Header with back button */}
<div className="p-4 border-b border-border flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<Button <Button
variant="ghost" variant="ghost"
onClick={onBackToApp} onClick={onBackToApp}
@ -176,7 +176,7 @@ function SettingsSidebar({
</nav> </nav>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t border-border"> <div className="p-4">
<p className="text-xs text-muted-foreground text-center">SurfSense Settings</p> <p className="text-xs text-muted-foreground text-center">SurfSense Settings</p>
</div> </div>
</aside> </aside>
@ -229,12 +229,12 @@ function SettingsContent({
initial={{ scale: 0.8, opacity: 0 }} initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, duration: 0.3 }} transition={{ delay: 0.1, duration: 0.3 }}
className="flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/10 shadow-sm shrink-0" className="flex items-center justify-center w-10 h-10 md:w-14 md:h-14 rounded-lg md:rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/10 shadow-sm shrink-0"
> >
<Icon className="h-6 w-6 md:h-7 md:w-7 text-primary" /> <Icon className="h-5 w-5 md:h-7 md:w-7 text-primary" />
</motion.div> </motion.div>
<div className="min-w-0"> <div className="min-w-0">
<h1 className="text-xl md:text-2xl font-bold tracking-tight truncate"> <h1 className="text-lg md:text-2xl font-bold tracking-tight truncate">
{activeItem?.label} {activeItem?.label}
</h1> </h1>
</div> </div>

View file

@ -51,11 +51,13 @@ export default function AddSourcesPage() {
> >
{/* Header */} {/* Header */}
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h1 className="text-4xl font-bold tracking-tight flex items-center justify-center gap-3"> <h1 className="text-2xl sm:text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
<Database className="h-8 w-8" /> <Database className="h-6 w-6 sm:h-8 sm:w-8" />
Add Sources Add Sources
</h1> </h1>
<p className="text-muted-foreground text-lg">Add your sources to your search space</p> <p className="text-muted-foreground text-sm sm:text-lg">
Add your sources to your search space
</p>
</div> </div>
{/* Tabs */} {/* Tabs */}

View file

@ -1,29 +1,15 @@
"use client"; "use client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import {
type ColumnDef,
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
ArrowLeft, ArrowLeft,
Calendar, Calendar,
Check, Check,
ChevronDown,
ChevronUp,
Clock, Clock,
Copy, Copy,
Crown, Crown,
Edit2, Edit2,
ExternalLink,
Hash, Hash,
Link2, Link2,
LinkIcon, LinkIcon,
@ -32,7 +18,6 @@ import {
Plus, Plus,
RefreshCw, RefreshCw,
Search, Search,
Settings,
Shield, Shield,
ShieldCheck, ShieldCheck,
Trash2, Trash2,
@ -40,7 +25,6 @@ import {
UserMinus, UserMinus,
UserPlus, UserPlus,
Users, Users,
X,
} from "lucide-react"; } from "lucide-react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@ -90,7 +74,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@ -105,7 +88,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { import {
Table, Table,
TableBody, TableBody,
@ -295,7 +277,7 @@ export default function TeamManagementPage() {
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}); });
const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom); const { data: permissionsData } = useAtomValue(permissionsAtom);
const permissions = permissionsData?.permissions || []; const permissions = permissionsData?.permissions || [];
const groupedPermissions = useMemo(() => { const groupedPermissions = useMemo(() => {
const groups: Record<string, typeof permissions> = {}; const groups: Record<string, typeof permissions> = {};
@ -308,8 +290,6 @@ export default function TeamManagementPage() {
return groups; return groups;
}, [permissions]); }, [permissions]);
const canManageMembers = hasPermission("members:view");
const canManageRoles = hasPermission("roles:read");
const canInvite = hasPermission("members:invite"); const canInvite = hasPermission("members:invite");
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
@ -339,40 +319,44 @@ export default function TeamManagementPage() {
variants={staggerContainer} variants={staggerContainer}
className="min-h-screen bg-background" className="min-h-screen bg-background"
> >
<div className="container max-w-7xl mx-auto p-6 lg:p-8"> <div className="container max-w-7xl mx-auto p-4 md:p-6 lg:p-8">
<motion.div variants={fadeInUp} className="space-y-8"> <motion.div variants={fadeInUp} className="space-y-8">
{/* Header */} {/* Header */}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-start space-x-3 md:items-center md:space-x-4">
<button <button
onClick={() => router.push(`/dashboard/${searchSpaceId}`)} onClick={() => router.push(`/dashboard/${searchSpaceId}`)}
className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors" className="flex items-center justify-center h-9 w-9 md:h-10 md:w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors shrink-0"
aria-label="Back to Dashboard" aria-label="Back to Dashboard"
type="button" type="button"
> >
<ArrowLeft className="h-5 w-5 text-primary" /> <ArrowLeft className="h-4 w-4 md:h-5 md:w-5 text-primary" />
</button> </button>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 ring-1 ring-primary/10"> <div className="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 ring-1 ring-primary/10 shrink-0">
<Users className="h-6 w-6 text-primary" /> <Users className="h-5 w-5 md:h-6 md:w-6 text-primary" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1 min-w-0">
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text"> <h1 className="text-2xl md:text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
Team Management Team Management
</h1> </h1>
<p className="text-muted-foreground"> <p className="text-xs md:text-sm text-muted-foreground">
Manage members, roles, and invite links for your search space Manage members, roles, and invite links for your search space
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button onClick={handleRefresh} variant="outline" size="sm" className="gap-2"> <Button
onClick={handleRefresh}
variant="outline"
size="sm"
className="gap-2 w-full md:w-auto"
>
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
Refresh Refresh
</Button> </Button>
</div> </div>
</div> </div>
<Separator className="bg-gradient-to-r from-border via-border/50 to-transparent" />
</div> </div>
{/* Summary Cards */} {/* Summary Cards */}
@ -435,42 +419,55 @@ export default function TeamManagementPage() {
{/* Tabs Content */} {/* Tabs Content */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<TabsList className="bg-muted/50 p-1"> <div className="overflow-x-auto pb-1 md:pb-0">
<TabsTrigger value="members" className="gap-2 data-[state=active]:bg-background"> <TabsList className="bg-muted/50 p-1 w-full md:w-fit grid grid-cols-3 md:flex">
<Users className="h-4 w-4" /> <TabsTrigger
<span>Members</span> value="members"
<Badge variant="secondary" className="ml-1 text-xs"> className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
{members.length} >
</Badge> <Users className="h-4 w-4 hidden md:block" />
</TabsTrigger> <span>Members</span>
<TabsTrigger value="roles" className="gap-2 data-[state=active]:bg-background"> <Badge variant="secondary" className="ml-1 text-xs">
<Shield className="h-4 w-4" /> {members.length}
<span>Roles</span> </Badge>
<Badge variant="secondary" className="ml-1 text-xs"> </TabsTrigger>
{roles.length} <TabsTrigger
</Badge> value="roles"
</TabsTrigger> className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
<TabsTrigger value="invites" className="gap-2 data-[state=active]:bg-background"> >
<LinkIcon className="h-4 w-4" /> <Shield className="h-4 w-4 hidden md:block" />
<span>Invites</span> <span>Roles</span>
<Badge variant="secondary" className="ml-1 text-xs"> <Badge variant="secondary" className="ml-1 text-xs">
{invites.filter((i) => i.is_active).length} {roles.length}
</Badge> </Badge>
</TabsTrigger> </TabsTrigger>
</TabsList> <TabsTrigger
value="invites"
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
>
<LinkIcon className="h-4 w-4 hidden md:block" />
<span>Invites</span>
<Badge variant="secondary" className="ml-1 text-xs">
{invites.filter((i) => i.is_active).length}
</Badge>
</TabsTrigger>
</TabsList>
</div>
{activeTab === "invites" && canInvite && ( {activeTab === "invites" && canInvite && (
<CreateInviteDialog <CreateInviteDialog
roles={roles} roles={roles}
onCreateInvite={handleCreateInvite} onCreateInvite={handleCreateInvite}
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
className="w-full md:w-auto"
/> />
)} )}
{activeTab === "roles" && hasPermission("roles:create") && ( {activeTab === "roles" && hasPermission("roles:create") && (
<CreateRoleDialog <CreateRoleDialog
groupedPermissions={groupedPermissions} groupedPermissions={groupedPermissions}
onCreateRole={handleCreateRole} onCreateRole={handleCreateRole}
className="w-full md:w-auto"
/> />
)} )}
</div> </div>
@ -533,8 +530,6 @@ function MembersTab({
canManageRoles: boolean; canManageRoles: boolean;
canRemove: boolean; canRemove: boolean;
}) { }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const filteredMembers = useMemo(() => { const filteredMembers = useMemo(() => {
@ -575,13 +570,13 @@ function MembersTab({
</div> </div>
{/* Members List */} {/* Members List */}
<div className="rounded-lg border bg-card overflow-hidden"> <div className="rounded-lg border bg-card overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-muted/50"> <TableRow className="bg-muted/50">
<TableHead className="w-[300px]">Member</TableHead> <TableHead className="w-auto md:w-[300px] px-2 md:px-4">Member</TableHead>
<TableHead>Role</TableHead> <TableHead className="px-2 md:px-4">Role</TableHead>
<TableHead>Joined</TableHead> <TableHead className="hidden md:table-cell">Joined</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@ -604,11 +599,11 @@ function MembersTab({
transition={{ delay: index * 0.05 }} transition={{ delay: index * 0.05 }}
className="group border-b transition-colors hover:bg-muted/50" className="group border-b transition-colors hover:bg-muted/50"
> >
<TableCell> <TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
<div className="flex items-center gap-3"> <div className="flex items-center gap-1.5 md:gap-3">
<div className="relative"> <div className="relative">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center ring-2 ring-background"> <div className="h-8 w-8 md:h-10 md:w-10 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center ring-2 ring-background">
<User className="h-5 w-5 text-primary" /> <User className="h-4 w-4 md:h-5 md:w-5 text-primary" />
</div> </div>
{member.is_owner && ( {member.is_owner && (
<div className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-amber-500 flex items-center justify-center ring-2 ring-background"> <div className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-amber-500 flex items-center justify-center ring-2 ring-background">
@ -616,12 +611,14 @@ function MembersTab({
</div> </div>
)} )}
</div> </div>
<div> <div className="min-w-0">
<p className="font-medium">{member.user_email || "Unknown"}</p> <p className="font-medium text-xs md:text-sm truncate">
{member.user_email || "Unknown"}
</p>
{member.is_owner && ( {member.is_owner && (
<Badge <Badge
variant="outline" variant="outline"
className="text-xs mt-1 bg-amber-500/10 text-amber-600 border-amber-500/20" className="text-[10px] md:text-xs mt-0.5 md:mt-1 bg-amber-500/10 text-amber-600 border-amber-500/20 hidden md:inline-flex"
> >
Owner Owner
</Badge> </Badge>
@ -629,7 +626,7 @@ function MembersTab({
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
{canManageRoles && !member.is_owner ? ( {canManageRoles && !member.is_owner ? (
<Select <Select
value={member.role_id?.toString() || "none"} value={member.role_id?.toString() || "none"}
@ -637,7 +634,7 @@ function MembersTab({
onUpdateRole(member.id, value === "none" ? null : Number(value)) onUpdateRole(member.id, value === "none" ? null : Number(value))
} }
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-full md:w-[180px] h-8 md:h-10 text-xs md:text-sm">
<SelectValue placeholder="Select role" /> <SelectValue placeholder="Select role" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -653,19 +650,22 @@ function MembersTab({
</SelectContent> </SelectContent>
</Select> </Select>
) : ( ) : (
<Badge variant="secondary" className="gap-1"> <Badge
<Shield className="h-3 w-3" /> variant="secondary"
className="gap-1 text-[10px] md:text-xs py-0 md:py-0.5"
>
<Shield className="h-2.5 w-2.5 md:h-3 md:w-3" />
{member.role?.name || "No role"} {member.role?.name || "No role"}
</Badge> </Badge>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell className="hidden md:table-cell">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
{new Date(member.joined_at).toLocaleDateString()} {new Date(member.joined_at).toLocaleDateString()}
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right py-2 px-2 md:py-4 md:px-4 align-middle">
{canRemove && !member.is_owner && ( {canRemove && !member.is_owner && (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
@ -962,11 +962,11 @@ function InvitesTab({
className={cn("relative overflow-hidden transition-all", isInactive && "opacity-60")} className={cn("relative overflow-hidden transition-all", isInactive && "opacity-60")}
> >
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1 min-w-0"> <div className="flex items-start md:items-center gap-4 flex-1 min-w-0">
<div <div
className={cn( className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center shrink-0", "h-10 w-10 md:h-12 md:w-12 rounded-xl flex items-center justify-center shrink-0",
invite.is_active && !isExpired && !isMaxedOut invite.is_active && !isExpired && !isMaxedOut
? "bg-emerald-500/20" ? "bg-emerald-500/20"
: "bg-muted" : "bg-muted"
@ -974,7 +974,7 @@ function InvitesTab({
> >
<Link2 <Link2
className={cn( className={cn(
"h-6 w-6", "h-5 w-5 md:h-6 md:w-6",
invite.is_active && !isExpired && !isMaxedOut invite.is_active && !isExpired && !isMaxedOut
? "text-emerald-600" ? "text-emerald-600"
: "text-muted-foreground" : "text-muted-foreground"
@ -991,7 +991,7 @@ function InvitesTab({
)} )}
{isMaxedOut && ( {isMaxedOut && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
Max uses reached Maxed
</Badge> </Badge>
)} )}
{!invite.is_active && !isExpired && !isMaxedOut && ( {!invite.is_active && !isExpired && !isMaxedOut && (
@ -1000,44 +1000,44 @@ function InvitesTab({
</Badge> </Badge>
)} )}
</div> </div>
<div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground flex-wrap"> <div className="flex flex-col md:flex-row md:items-center gap-2 md:gap-4 mt-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Shield className="h-3 w-3" /> <Shield className="h-3 w-3" />
{invite.role?.name || "Default role"} {invite.role?.name || "Default role"}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Hash className="h-3 w-3" /> <Hash className="h-3 w-3" />
{invite.uses_count} uses {invite.uses_count}
{invite.max_uses && ` / ${invite.max_uses}`} {invite.max_uses ? ` / ${invite.max_uses} uses` : " uses"}
</span> </span>
{invite.expires_at && ( {invite.expires_at && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{isExpired {isExpired
? "Expired" ? "Expired"
: `Expires ${new Date(invite.expires_at).toLocaleDateString()}`} : `Exp: ${new Date(invite.expires_at).toLocaleDateString()}`}
</span> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0 self-end md:self-auto">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-2" className="gap-2 flex-1 md:flex-none"
onClick={() => copyInviteLink(invite)} onClick={() => copyInviteLink(invite)}
disabled={Boolean(isInactive)} disabled={Boolean(isInactive)}
> >
{copiedId === invite.id ? ( {copiedId === invite.id ? (
<> <>
<Check className="h-4 w-4 text-emerald-500" /> <Check className="h-4 w-4 text-emerald-500" />
Copied! <span className="md:inline">Copied!</span>
</> </>
) : ( ) : (
<> <>
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
Copy Link <span className="md:inline">Copy</span>
</> </>
)} )}
</Button> </Button>
@ -1088,11 +1088,11 @@ function InvitesTab({
function CreateInviteDialog({ function CreateInviteDialog({
roles, roles,
onCreateInvite, onCreateInvite,
searchSpaceId, className,
}: { }: {
roles: Role[]; roles: Role[];
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>; onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
searchSpaceId: number; className?: string;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
@ -1142,12 +1142,12 @@ function CreateInviteDialog({
return ( return (
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}> <Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="gap-2"> <Button className={cn("gap-2", className)}>
<UserPlus className="h-4 w-4" /> <UserPlus className="h-4 w-4" />
Create Invite Create Invite
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-md"> <DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6">
{createdInvite ? ( {createdInvite ? (
<> <>
<DialogHeader> <DialogHeader>
@ -1159,7 +1159,7 @@ function CreateInviteDialog({
Share this link to invite people to your search space. Share this link to invite people to your search space.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-3 py-2 md:py-4">
<div className="flex items-center gap-2 p-3 bg-muted rounded-lg"> <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
<code className="flex-1 min-w-0 text-sm break-all"> <code className="flex-1 min-w-0 text-sm break-all">
{window.location.origin}/invite/{createdInvite.invite_code} {window.location.origin}/invite/{createdInvite.invite_code}
@ -1203,7 +1203,7 @@ function CreateInviteDialog({
Create a link to invite people to this search space. Create a link to invite people to this search space.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-3 py-2 md:py-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="invite-name">Name (optional)</Label> <Label htmlFor="invite-name">Name (optional)</Label>
<Input <Input
@ -1234,7 +1234,7 @@ function CreateInviteDialog({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="flex flex-col md:grid md:grid-cols-2 gap-3 md:gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="max-uses">Max uses (optional)</Label> <Label htmlFor="max-uses">Max uses (optional)</Label>
<Input <Input
@ -1301,9 +1301,11 @@ function CreateInviteDialog({
function CreateRoleDialog({ function CreateRoleDialog({
groupedPermissions, groupedPermissions,
onCreateRole, onCreateRole,
className,
}: { }: {
groupedPermissions: Record<string, { value: string; name: string; category: string }[]>; groupedPermissions: Record<string, { value: string; name: string; category: string }[]>;
onCreateRole: (data: CreateRoleRequest["data"]) => Promise<Role>; onCreateRole: (data: CreateRoleRequest["data"]) => Promise<Role>;
className?: string;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
@ -1358,20 +1360,20 @@ function CreateRoleDialog({
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="gap-2"> <Button className={cn("gap-2", className)}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Create Role Create Role
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-xl p-4 md:p-6">
<DialogHeader> <DialogHeader>
<DialogTitle>Create Custom Role</DialogTitle> <DialogTitle>Create Custom Role</DialogTitle>
<DialogDescription> <DialogDescription className="text-xs md:text-sm">
Define a new role with specific permissions for this search space. Define a new role with specific permissions for this search space.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-3 py-2 md:py-4">
<div className="grid grid-cols-2 gap-4"> <div className="flex flex-col md:grid md:grid-cols-2 gap-3 md:gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="role-name">Role Name *</Label> <Label htmlFor="role-name">Role Name *</Label>
<Input <Input

View file

@ -7,7 +7,6 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms";
@ -38,7 +37,6 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Spotlight } from "@/components/ui/spotlight"; import { Spotlight } from "@/components/ui/spotlight";
import { Tilt } from "@/components/ui/tilt"; import { Tilt } from "@/components/ui/tilt";
import { authenticatedFetch } from "@/lib/auth-utils";
/** /**
* Formats a date string into a readable format * Formats a date string into a readable format
@ -65,7 +63,7 @@ const LoadingScreen = () => {
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<Card className="w-[350px] bg-background/60 backdrop-blur-sm"> <Card className="w-full max-w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">{t("loading")}</CardTitle> <CardTitle className="text-xl font-medium">{t("loading")}</CardTitle>
<CardDescription>{t("fetching_spaces")}</CardDescription> <CardDescription>{t("fetching_spaces")}</CardDescription>
@ -101,7 +99,7 @@ const ErrorScreen = ({ message }: { message: string }) => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<Card className="w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20"> <Card className="w-full max-w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" /> <AlertCircle className="h-5 w-5 text-destructive" />
@ -185,21 +183,21 @@ const DashboardPage = () => {
return ( return (
<motion.div <motion.div
className="container mx-auto py-10" className="container mx-auto py-6 md:py-10 px-4"
initial="hidden" initial="hidden"
animate="visible" animate="visible"
variants={containerVariants} variants={containerVariants}
> >
<motion.div className="flex flex-col space-y-6" variants={itemVariants}> <motion.div className="flex flex-col space-y-4 md:space-y-6" variants={itemVariants}>
<div className="flex flex-row space-x-4 justify-between"> <div className="flex flex-row items-center justify-between gap-2">
<div className="flex flex-row space-x-4"> <div className="flex flex-row items-center md:space-x-4">
<Logo className="w-10 h-10 rounded-md" /> <Logo className="w-8 h-8 md:w-10 md:h-10 rounded-md shrink-0 hidden md:block" />
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-0.5 md:space-y-2">
<h1 className="text-4xl font-bold">{t("surfsense_dashboard")}</h1> <h1 className="text-xl md:text-4xl font-bold">{t("surfsense_dashboard")}</h1>
<p className="text-muted-foreground">{t("welcome_message")}</p> <p className="text-sm md:text-base text-muted-foreground">{t("welcome_message")}</p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-2 md:space-x-3 shrink-0">
<UserDropdown user={customUser} /> <UserDropdown user={customUser} />
<ThemeTogglerComponent /> <ThemeTogglerComponent />
</div> </div>
@ -207,18 +205,18 @@ const DashboardPage = () => {
<div className="flex flex-col space-y-6 mt-6"> <div className="flex flex-col space-y-6 mt-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-2xl font-semibold">{t("your_search_spaces")}</h2> <h2 className="text-lg md:text-2xl font-semibold">{t("your_search_spaces")}</h2>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Link href="/dashboard/searchspaces"> <Link href="/dashboard/searchspaces">
<Button className="h-10"> <Button className="h-8 md:h-10 text-[11px] md:text-sm px-3 md:px-4">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("create_search_space")} {t("create_search_space")}
</Button> </Button>
</Link> </Link>
</motion.div> </motion.div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{searchSpaces && {searchSpaces &&
searchSpaces.length > 0 && searchSpaces.length > 0 &&
searchSpaces.map((space) => ( searchSpaces.map((space) => (
@ -295,14 +293,17 @@ const DashboardPage = () => {
<div className="flex flex-1 flex-col justify-between p-1"> <div className="flex flex-1 flex-col justify-between p-1">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-medium text-lg">{space.name}</h3> <h3 className="font-medium text-base md:text-lg">{space.name}</h3>
{!space.is_owner && ( {!space.is_owner && (
<Badge variant="secondary" className="text-xs font-normal"> <Badge
variant="secondary"
className="text-[10px] md:text-xs font-normal"
>
{t("shared")} {t("shared")}
</Badge> </Badge>
)} )}
</div> </div>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-xs md:text-sm text-muted-foreground">
{space.description} {space.description}
</p> </p>
</div> </div>
@ -334,8 +335,10 @@ const DashboardPage = () => {
<div className="rounded-full bg-muted/50 p-4 mb-4"> <div className="rounded-full bg-muted/50 p-4 mb-4">
<Search className="h-8 w-8 text-muted-foreground" /> <Search className="h-8 w-8 text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-medium mb-2">{t("no_spaces_found")}</h3> <h3 className="text-base md:text-lg font-medium mb-2">{t("no_spaces_found")}</h3>
<p className="text-muted-foreground mb-6">{t("create_first_space")}</p> <p className="text-xs md:text-sm text-muted-foreground mb-6">
{t("create_first_space")}
</p>
<Link href="/dashboard/searchspaces"> <Link href="/dashboard/searchspaces">
<Button> <Button>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
@ -359,8 +362,10 @@ const DashboardPage = () => {
> >
<Link href="/dashboard/searchspaces" className="flex h-full"> <Link href="/dashboard/searchspaces" className="flex h-full">
<div className="flex flex-col items-center justify-center h-full w-full rounded-xl border border-dashed bg-muted/10 hover:border-primary/50 transition-colors"> <div className="flex flex-col items-center justify-center h-full w-full rounded-xl border border-dashed bg-muted/10 hover:border-primary/50 transition-colors">
<Plus className="h-10 w-10 mb-3 text-muted-foreground" /> <Plus className="h-8 w-8 md:h-10 md:w-10 mb-2 md:mb-3 text-muted-foreground" />
<span className="text-sm font-medium">{t("add_new_search_space")}</span> <span className="text-xs md:text-sm font-medium">
{t("add_new_search_space")}
</span>
</div> </div>
</Link> </Link>
</Tilt> </Tilt>

View file

@ -159,3 +159,4 @@ button {
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'; @source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
@source '../node_modules/streamdown/dist/*.js'; @source '../node_modules/streamdown/dist/*.js';

View file

@ -162,5 +162,52 @@ export default function BlockNoteEditor({
}, [resolvedTheme]); }, [resolvedTheme]);
// Renders the editor instance // 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 ( return (
<Select value={locale} onValueChange={handleLanguageChange}> <Select value={locale} onValueChange={handleLanguageChange}>
<SelectTrigger className="w-[160px]"> <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="mr-2 h-4 w-4" /> <Globe className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
<SelectValue> <SelectValue>
{languages.find((lang) => lang.code === locale)?.name || "English"} {languages.find((lang) => lang.code === locale)?.name || "English"}
</SelectValue> </SelectValue>

View file

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

View file

@ -154,8 +154,8 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
"text-muted-foreground hover:text-foreground" "text-muted-foreground hover:text-foreground"
)} )}
> >
{/* Header text with shimmer if processing or has in-progress step */} {/* Header text with shimmer if processing (streaming) */}
{isProcessing || inProgressStep ? ( {isProcessing ? (
<TextShimmerLoader text={getHeaderText()} size="sm" /> <TextShimmerLoader text={getHeaderText()} size="sm" />
) : ( ) : (
<span>{getHeaderText()}</span> <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"> <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 */} {/* 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"> <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} {greeting}
</h1> </h1>
</div> </div>
@ -891,14 +891,17 @@ const ThinkingStepsPart: FC = () => {
const messageId = useAssistantState(({ message }) => message?.id); const messageId = useAssistantState(({ message }) => message?.id);
const thinkingSteps = thinkingStepsMap.get(messageId) || []; 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 isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
const isMessageStreaming = isThreadRunning && isLastMessage;
if (thinkingSteps.length === 0) return null; if (thinkingSteps.length === 0) return null;
return ( return (
<div className="mb-3"> <div className="mb-3">
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isThreadRunning} /> <ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isMessageStreaming} />
</div> </div>
); );
}; };

View file

@ -2,7 +2,7 @@
import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react"; import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import Link from "next/link"; import Link from "next/link";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle"; import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
import { useGithubStars } from "@/hooks/use-github-stars"; import { useGithubStars } from "@/hooks/use-github-stars";
@ -118,89 +118,88 @@ const MobileNav = ({ navItems, isScrolled }: any) => {
const { compactFormat: githubStars, loading: loadingGithubStars } = useGithubStars(); const { compactFormat: githubStars, loading: loadingGithubStars } = useGithubStars();
return ( return (
<> <motion.div
<motion.div animate={{ borderRadius: open ? "4px" : "2rem" }}
animate={{ borderRadius: open ? "4px" : "2rem" }} key={String(open)}
key={String(open)} className={cn(
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",
"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
isScrolled ? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
? "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"
: "bg-transparent border border-transparent" )}
)} >
> <div className="flex w-full flex-row items-center justify-between">
<div className="flex w-full flex-row items-center justify-between"> <div className="flex flex-row items-center gap-2">
<div className="flex flex-row items-center gap-2"> <Logo className="h-8 w-8 rounded-md" />
<Logo className="h-8 w-8 rounded-md" /> <span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
<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>
</div> </div>
<button
<AnimatePresence> type="button"
{open && ( onClick={() => setOpen(!open)}
<motion.div 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"
initial={{ opacity: 0 }} aria-label={open ? "Close menu" : "Open menu"}
animate={{ opacity: 1 }} >
exit={{ opacity: 0 }} {open ? (
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" <IconX className="h-6 w-6 text-black dark:text-white" />
> ) : (
{navItems.map((navItem: any, idx: number) => ( <IconMenu2 className="h-6 w-6 text-black dark:text-white" />
<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> </button>
</motion.div> </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="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="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"> <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 Parameter Key
</Label> </Label>
<Select value={selectedKey} onValueChange={setSelectedKey}> <Select value={selectedKey} onValueChange={setSelectedKey}>
@ -87,7 +87,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
</div> </div>
<div className="flex flex-col space-y-1"> <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 Value
</Label> </Label>
<Input <Input
@ -100,11 +100,11 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
</div> </div>
<Button <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} onClick={handleAdd}
disabled={!selectedKey || value === ""} 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> </Button>
</div> </div>

View file

@ -47,11 +47,13 @@ export function JsonMetadataViewer({
if (open !== undefined && onOpenChange !== undefined) { if (open !== undefined && onOpenChange !== undefined) {
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <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> <DialogHeader>
<DialogTitle>{title} - Metadata</DialogTitle> <DialogTitle className="text-base sm:text-lg truncate pr-6">
{title} - Metadata
</DialogTitle>
</DialogHeader> </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} /> <JsonView data={jsonData} style={defaultStyles} />
</div> </div>
</DialogContent> </DialogContent>
@ -70,11 +72,13 @@ export function JsonMetadataViewer({
</Button> </Button>
)} )}
</DialogTrigger> </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> <DialogHeader>
<DialogTitle>{title} - Metadata</DialogTitle> <DialogTitle className="text-base sm:text-lg truncate pr-6">
{title} - Metadata
</DialogTitle>
</DialogHeader> </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} /> <JsonView data={jsonData} style={defaultStyles} />
</div> </div>
</DialogContent> </DialogContent>

View file

@ -184,8 +184,8 @@ export const DocumentMentionPicker = forwardRef<
role="listbox" role="listbox"
tabIndex={-1} tabIndex={-1}
> >
{/* Document List */} {/* Document List - Shows max 3 items on mobile, 5 items on desktop */}
<div className="max-h-[280px] overflow-y-auto"> <div className="max-h-[108px] sm:max-h-[180px] overflow-y-auto">
{actualLoading ? ( {actualLoading ? (
<div className="flex items-center justify-center py-4"> <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" /> <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" /> <Bot className="size-5 text-primary" />
</div> </div>
<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"> <div className="flex items-center gap-2 mt-0.5">
{isGlobal ? ( {isGlobal ? (
<Badge variant="secondary" className="gap-1 text-xs"> <Badge variant="secondary" className="gap-1 text-xs">
@ -207,9 +207,10 @@ export function ModelConfigSidebar({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => onOpenChange(false)} 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> </Button>
</div> </div>

View file

@ -175,39 +175,44 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( 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", "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", "focus-visible:ring-0 focus-visible:ring-offset-0",
className className
)} )}
> >
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="size-4 animate-spin text-muted-foreground" /> <Loader2 className="size-3.5 md:size-4 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">Loading...</span> <span className="text-muted-foreground hidden md:inline">Loading...</span>
<span className="text-muted-foreground md:hidden">Load...</span>
</> </>
) : currentConfig ? ( ) : currentConfig ? (
<> <>
{getProviderIcon(currentConfig.provider)} {getProviderIcon(currentConfig.provider)}
<span className="max-w-[150px] truncate">{currentConfig.name}</span> <span className="max-w-[80px] md: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"> <Badge
{currentConfig.model_name.split("/").pop()?.slice(0, 15) || variant="secondary"
currentConfig.model_name.slice(0, 15)} 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> </Badge>
</> </>
) : ( ) : (
<> <>
<Bot className="size-4 text-muted-foreground" /> <Bot className="size-3.5 md:size-4 text-muted-foreground" />
<span className="text-muted-foreground">Select Model</span> <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> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <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" align="start"
sideOffset={8} sideOffset={8}
> >
@ -225,17 +230,17 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</div> </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 <CommandInput
placeholder="Search models..." placeholder="Search models..."
value={searchQuery} value={searchQuery}
onValueChange={setSearchQuery} 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} disabled={isSwitching}
/> />
</div> </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"> <CommandEmpty className="py-8 text-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<Bot className="size-8 text-muted-foreground/40" /> <Bot className="size-8 text-muted-foreground/40" />

View file

@ -352,9 +352,9 @@ export function SourceDetailPanel({
size="icon" size="icon"
variant="ghost" variant="ghost"
onClick={() => onOpenChange(false)} 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> <span className="sr-only">Close</span>
</Button> </Button>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,7 +81,7 @@ export function ConnectorsTab({ searchSpaceId }: ConnectorsTabProps) {
className="w-full" className="w-full"
> >
<div className="flex items-center justify-between space-x-4 p-4"> <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> <CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted"> <Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted">
<motion.div <motion.div

View file

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

View file

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

View file

@ -6,6 +6,88 @@ import { z } from "zod";
export const TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]); export const TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]);
export type TodoStatus = z.infer<typeof TodoStatusSchema>; 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 * Single todo item in a plan
* Matches deepagents TodoListMiddleware output: { content, status } * Matches deepagents TodoListMiddleware output: { content, status }
@ -67,9 +149,7 @@ export function parseSerializablePlan(data: unknown): NormalizedPlan {
return { return {
id: typeof todo?.id === "string" ? todo.id : `todo-${i}`, id: typeof todo?.id === "string" ? todo.id : `todo-${i}`,
content: typeof todo?.content === "string" ? todo.content : "Task", content: typeof todo?.content === "string" ? todo.content : "Task",
status: TodoStatusSchema.safeParse(todo?.status).success status: normalizeStatus(todo?.status),
? (todo.status as TodoStatus)
: ("pending" as const),
}; };
}) })
: [{ id: "1", content: "No tasks", status: "pending" as const }], : [{ id: "1", content: "No tasks", status: "pending" as const }],

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}