mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
Merge branch 'dev' of https://github.com/MODSetter/SurfSense into dev
This commit is contained in:
commit
35904ba0c8
43 changed files with 1019 additions and 663 deletions
|
|
@ -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.
|
||||
* 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.
|
||||
- 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
|
||||
6. write_todos: Create and update a planning/todo list.
|
||||
- Args:
|
||||
- todos: List of todo items, each with:
|
||||
* content: Description of the task (required)
|
||||
* status: "pending", "in_progress", or "completed" (required)
|
||||
- The tool automatically adds IDs and formats the output for the UI.
|
||||
* status: "pending", "in_progress", "completed", or "cancelled" (required)
|
||||
|
||||
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>
|
||||
<tool_call_examples>
|
||||
- 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")`
|
||||
- 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"
|
||||
- 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
|
||||
|
|
@ -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"}])`
|
||||
- Then provide travel preparation guidance
|
||||
|
||||
- User: "Break down how to learn guitar"
|
||||
- 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"}])`
|
||||
- Then provide learning milestones and tips
|
||||
- COMPLETE WORKFLOW EXAMPLE - User: "Explain how to set up a Python project"
|
||||
- STEP 1 (Create initial plan):
|
||||
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"
|
||||
- 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
|
||||
[MODE B EXAMPLES] External Tasks - preserve original status, you CANNOT complete:
|
||||
|
||||
- User: "Help me organize my home renovation project"
|
||||
- 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"}])`
|
||||
- Then provide detailed renovation guidance
|
||||
- User: "Show my Linear tasks" or "Create todos for Linear tasks"
|
||||
- First search: `search_knowledge_base(query="Linear tasks issues", connectors_to_search=["LINEAR_CONNECTOR"])`
|
||||
- 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?"
|
||||
- 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"}])`
|
||||
- Then provide podcast launch guidance
|
||||
- User: "List my Jira tickets"
|
||||
- First search: `search_knowledge_base(query="Jira tickets issues", connectors_to_search=["JIRA_CONNECTOR"])`
|
||||
- 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>
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ export function GoogleLoginButton() {
|
|||
return (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<AmbientBackground />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||
<Logo className="rounded-full my-8" />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
|
||||
<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">
|
||||
Login
|
||||
</h1> */}
|
||||
|
|
@ -93,7 +93,7 @@ export function GoogleLoginButton() {
|
|||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
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}
|
||||
>
|
||||
<div className="absolute inset-0 h-full w-full transform opacity-0 transition duration-200 group-hover/btn:opacity-100">
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@ export function LocalLoginForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="w-full max-w-md px-6 md:px-0">
|
||||
<form onSubmit={handleSubmit} className="space-y-3 md:space-y-4">
|
||||
{/* Error Display */}
|
||||
<AnimatePresence>
|
||||
{error && error.title && (
|
||||
|
|
@ -194,7 +194,7 @@ export function LocalLoginForm() {
|
|||
required
|
||||
value={username}
|
||||
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
|
||||
? "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"
|
||||
|
|
@ -217,7 +217,7 @@ export function LocalLoginForm() {
|
|||
required
|
||||
value={password}
|
||||
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
|
||||
? "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"
|
||||
|
|
@ -238,7 +238,7 @@ export function LocalLoginForm() {
|
|||
<button
|
||||
type="submit"
|
||||
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")}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function LoginContent() {
|
|||
// Get the auth type from environment variables
|
||||
setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE");
|
||||
setIsLoading(false);
|
||||
}, [searchParams]);
|
||||
}, [searchParams, t, tCommon]);
|
||||
|
||||
// Show loading state while determining auth type
|
||||
if (isLoading) {
|
||||
|
|
@ -111,8 +111,8 @@ function LoginContent() {
|
|||
<div className="relative w-full overflow-hidden">
|
||||
<AmbientBackground />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||
<Logo className="rounded-md" />
|
||||
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||
<Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
|
||||
<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")}
|
||||
</h1>
|
||||
|
||||
|
|
|
|||
|
|
@ -157,17 +157,17 @@ export default function RegisterPage() {
|
|||
return (
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<AmbientBackground />
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||
<Logo className="rounded-md" />
|
||||
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
|
||||
<Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
|
||||
<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")}
|
||||
</h1>
|
||||
|
||||
<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 */}
|
||||
<AnimatePresence>
|
||||
{error && error.title && (
|
||||
{error?.title && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
|
|
@ -239,7 +239,7 @@ export default function RegisterPage() {
|
|||
required
|
||||
value={email}
|
||||
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
|
||||
? "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"
|
||||
|
|
@ -261,7 +261,7 @@ export default function RegisterPage() {
|
|||
required
|
||||
value={password}
|
||||
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
|
||||
? "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"
|
||||
|
|
@ -283,7 +283,7 @@ export default function RegisterPage() {
|
|||
required
|
||||
value={confirmPassword}
|
||||
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
|
||||
? "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"
|
||||
|
|
@ -295,7 +295,7 @@ export default function RegisterPage() {
|
|||
<button
|
||||
type="submit"
|
||||
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")}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ export function DashboardClientLayout({
|
|||
|
||||
return (
|
||||
<SidebarProvider
|
||||
className="h-full bg-red-600 overflow-hidden"
|
||||
className="h-full overflow-hidden"
|
||||
open={open}
|
||||
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 gap-2">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<DashboardBreadcrumb />
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<DashboardBreadcrumb />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSwitcher />
|
||||
|
|
|
|||
|
|
@ -278,14 +278,17 @@ export default function ConnectorsPage() {
|
|||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mb-8 flex items-center justify-between"
|
||||
className="mb-8 flex items-center justify-between gap-2"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
|
||||
<p className="text-muted-foreground mt-2">{t("subtitle")}</p>
|
||||
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
|
||||
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Button
|
||||
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")}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -75,14 +75,14 @@ export function DocumentsFilters({
|
|||
|
||||
return (
|
||||
<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 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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
|
||||
className="relative"
|
||||
className="relative w-full sm:w-auto"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
|
|
@ -90,7 +90,7 @@ export function DocumentsFilters({
|
|||
<Input
|
||||
id={`${id}-input`}
|
||||
ref={inputRef}
|
||||
className="peer min-w-60 ps-9"
|
||||
className="peer w-full sm:min-w-60 ps-9"
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
placeholder={t("filter_placeholder")}
|
||||
|
|
@ -231,11 +231,11 @@ export function DocumentsFilters({
|
|||
</DropdownMenu>
|
||||
</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 && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button className="ml-auto" variant="outline">
|
||||
<Button className="w-full sm:w-auto" variant="outline">
|
||||
<Trash
|
||||
className="-ms-1 me-2 opacity-60"
|
||||
size={16}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,14 @@ export function DocumentsTableShell({
|
|||
|
||||
const toggleAll = (checked: boolean) => {
|
||||
const next = new Set(selectedIds);
|
||||
if (checked) sorted.forEach((d) => next.add(d.id));
|
||||
else sorted.forEach((d) => next.delete(d.id));
|
||||
if (checked)
|
||||
sorted.forEach((d) => {
|
||||
next.add(d.id);
|
||||
});
|
||||
else
|
||||
sorted.forEach((d) => {
|
||||
next.delete(d.id);
|
||||
});
|
||||
setSelectedIds(next);
|
||||
};
|
||||
|
||||
|
|
@ -323,26 +329,16 @@ export function DocumentsTableShell({
|
|||
const icon = getDocumentTypeIcon(doc.document_type);
|
||||
return (
|
||||
<div key={doc.id} className="p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={selectedIds.has(doc.id)}
|
||||
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
<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">
|
||||
<span className="text-muted-foreground shrink-0">{icon}</span>
|
||||
<div className="font-medium truncate">{doc.title}</div>
|
||||
</div>
|
||||
<RowActions
|
||||
document={doc}
|
||||
deleteDocument={deleteDocument}
|
||||
refreshDocuments={async () => {
|
||||
await onRefresh();
|
||||
}}
|
||||
searchSpaceId={searchSpaceId as string}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-muted-foreground shrink-0">{icon}</span>
|
||||
<div className="font-medium truncate">{doc.title}</div>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<DocumentTypeChip type={doc.document_type} />
|
||||
|
|
@ -371,6 +367,14 @@ export function DocumentsTableShell({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RowActions
|
||||
document={doc}
|
||||
deleteDocument={deleteDocument}
|
||||
refreshDocuments={async () => {
|
||||
await onRefresh();
|
||||
}}
|
||||
searchSpaceId={searchSpaceId as string}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import { useTranslations } from "next-intl";
|
|||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
interface ProcessingIndicatorProps {
|
||||
activeTasksCount: number;
|
||||
documentProcessorTasksCount: number;
|
||||
}
|
||||
|
||||
export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorProps) {
|
||||
export function ProcessingIndicator({ documentProcessorTasksCount }: ProcessingIndicatorProps) {
|
||||
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 (
|
||||
<AnimatePresence>
|
||||
|
|
@ -32,7 +33,7 @@ export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorPro
|
|||
{t("processing_documents")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-muted-foreground">
|
||||
{t("active_tasks_count", { count: activeTasksCount })}
|
||||
{t("active_tasks_count", { count: documentProcessorTasksCount })}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { FileText, Pencil, Trash2 } from "lucide-react";
|
||||
import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
|
@ -16,6 +16,12 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
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 type { Document } from "./types";
|
||||
|
||||
|
|
@ -57,53 +63,108 @@ export function RowActions({
|
|||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{/* Edit 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-foreground hover:bg-muted/80"
|
||||
onClick={handleEdit}
|
||||
{/* Desktop Actions */}
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span className="sr-only">Edit Document</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Edit Document</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<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" />
|
||||
<span className="sr-only">Edit Document</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Edit Document</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* View Metadata 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-foreground hover:bg-muted/80"
|
||||
onClick={() => setIsMetadataOpen(true)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="sr-only">View Metadata</span>
|
||||
<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" />
|
||||
<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>
|
||||
</motion.div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>View Metadata</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
</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
|
||||
title={document.title}
|
||||
metadata={document.document_metadata}
|
||||
|
|
@ -111,30 +172,6 @@ export function RowActions({
|
|||
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}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
|
|
|||
|
|
@ -139,6 +139,14 @@ export default function DocumentsTable() {
|
|||
enablePolling: true,
|
||||
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 prevActiveTasksCount = useRef(activeTasksCount);
|
||||
|
||||
|
|
@ -220,8 +228,8 @@ export default function DocumentsTable() {
|
|||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-muted-foreground">{t("subtitle")}</p>
|
||||
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={refreshCurrentView} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
|
|
@ -229,7 +237,7 @@ export default function DocumentsTable() {
|
|||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<ProcessingIndicator activeTasksCount={activeTasksCount} />
|
||||
<ProcessingIndicator documentProcessorTasksCount={documentProcessorTasksCount} />
|
||||
|
||||
<DocumentsFilters
|
||||
typeCounts={typeCounts ?? {}}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||
|
||||
|
|
@ -130,7 +129,7 @@ export default function EditorPage() {
|
|||
setError(null);
|
||||
setHasUnsavedChanges(false);
|
||||
setLoading(true);
|
||||
}, [documentId]);
|
||||
}, []);
|
||||
|
||||
// Fetch document content - DIRECT CALL TO FASTAPI
|
||||
// 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"
|
||||
>
|
||||
{/* 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="flex items-center gap-3 flex-1 min-w-0">
|
||||
<FileText className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||
<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-2 md:gap-3 flex-1 min-w-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">
|
||||
<h1 className="text-lg font-semibold truncate">{displayTitle}</h1>
|
||||
{hasUnsavedChanges && <p className="text-xs text-muted-foreground">Unsaved changes</p>}
|
||||
<h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1>
|
||||
{hasUnsavedChanges && (
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground">Unsaved changes</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={saving} className="gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
<Button
|
||||
variant="outline"
|
||||
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 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 ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isNewNote ? "Creating..." : "Saving..."}
|
||||
<Loader2 className="h-3.5 w-3.5 md:h-4 md:w-4 animate-spin" />
|
||||
<span className="text-xs md:text-sm">
|
||||
{isNewNote ? "Creating..." : "Saving..."}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
Save
|
||||
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
<span className="text-xs md:text-sm">Save</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -459,7 +471,7 @@ export default function EditorPage() {
|
|||
|
||||
{/* Editor Container */}
|
||||
<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 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
|
|
|
|||
|
|
@ -506,8 +506,8 @@ export default function LogsManagePage() {
|
|||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-muted-foreground">{t("subtitle")}</p>
|
||||
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
|
||||
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
|
||||
</div>
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
|
|
@ -521,48 +521,10 @@ export default function LogsManagePage() {
|
|||
uniqueLevels={uniqueLevels}
|
||||
uniqueStatuses={uniqueStatuses}
|
||||
inputRef={inputRef}
|
||||
onBulkDelete={handleDeleteRows}
|
||||
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 */}
|
||||
<LogsTable
|
||||
table={table}
|
||||
|
|
@ -713,29 +675,31 @@ function LogsFilters({
|
|||
uniqueLevels,
|
||||
uniqueStatuses,
|
||||
inputRef,
|
||||
onBulkDelete,
|
||||
id,
|
||||
}: {
|
||||
table: any;
|
||||
uniqueLevels: string[];
|
||||
uniqueStatuses: string[];
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
onBulkDelete: () => Promise<void>;
|
||||
id: string;
|
||||
}) {
|
||||
const t = useTranslations("logs");
|
||||
return (
|
||||
<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 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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 */}
|
||||
<motion.div className="relative" variants={fadeInScale}>
|
||||
<motion.div className="relative w-full sm:w-auto" variants={fadeInScale}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
"peer min-w-60 ps-9",
|
||||
"peer w-full sm:min-w-60 ps-9",
|
||||
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
|
||||
)}
|
||||
value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
|
||||
|
|
@ -806,6 +770,39 @@ function LogsFilters({
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -973,6 +970,7 @@ function LogsTable({
|
|||
style={{ width: `${header.getSize()}px` }}
|
||||
className={cn(
|
||||
"h-12 px-4 py-3",
|
||||
header.column.id === "select" ? "ps-4 pe-0" : "",
|
||||
// keep Created At header from wrapping and align it
|
||||
header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
|
||||
)}
|
||||
|
|
@ -1030,7 +1028,8 @@ function LogsTable({
|
|||
<TableCell
|
||||
key={cell.id}
|
||||
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
|
||||
? "whitespace-nowrap text-xs text-muted-foreground text-right"
|
||||
: "",
|
||||
|
|
|
|||
|
|
@ -87,14 +87,14 @@ function SettingsSidebar({
|
|||
<aside
|
||||
className={cn(
|
||||
"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",
|
||||
"md:translate-x-0",
|
||||
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
||||
)}
|
||||
>
|
||||
{/* 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
|
||||
variant="ghost"
|
||||
onClick={onBackToApp}
|
||||
|
|
@ -176,7 +176,7 @@ function SettingsSidebar({
|
|||
</nav>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
@ -229,12 +229,12 @@ function SettingsContent({
|
|||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
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>
|
||||
<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}
|
||||
</h1>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,11 +51,13 @@ export default function AddSourcesPage() {
|
|||
>
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
|
||||
<Database className="h-8 w-8" />
|
||||
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
|
||||
<Database className="h-6 w-6 sm:h-8 sm:w-8" />
|
||||
Add Sources
|
||||
</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>
|
||||
|
||||
{/* Tabs */}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
"use client";
|
||||
|
||||
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 {
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Copy,
|
||||
Crown,
|
||||
Edit2,
|
||||
ExternalLink,
|
||||
Hash,
|
||||
Link2,
|
||||
LinkIcon,
|
||||
|
|
@ -32,7 +18,6 @@ import {
|
|||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
|
|
@ -40,7 +25,6 @@ import {
|
|||
UserMinus,
|
||||
UserPlus,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
|
@ -90,7 +74,6 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
|
@ -105,7 +88,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -295,7 +277,7 @@ export default function TeamManagementPage() {
|
|||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom);
|
||||
const { data: permissionsData } = useAtomValue(permissionsAtom);
|
||||
const permissions = permissionsData?.permissions || [];
|
||||
const groupedPermissions = useMemo(() => {
|
||||
const groups: Record<string, typeof permissions> = {};
|
||||
|
|
@ -308,8 +290,6 @@ export default function TeamManagementPage() {
|
|||
return groups;
|
||||
}, [permissions]);
|
||||
|
||||
const canManageMembers = hasPermission("members:view");
|
||||
const canManageRoles = hasPermission("roles:read");
|
||||
const canInvite = hasPermission("members:invite");
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
|
|
@ -339,40 +319,44 @@ export default function TeamManagementPage() {
|
|||
variants={staggerContainer}
|
||||
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">
|
||||
{/* Header */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-start space-x-3 md:items-center md:space-x-4">
|
||||
<button
|
||||
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"
|
||||
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>
|
||||
<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">
|
||||
<Users className="h-6 w-6 text-primary" />
|
||||
<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-5 w-5 md:h-6 md:w-6 text-primary" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gradient-to-r from-border via-border/50 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
|
|
@ -435,42 +419,55 @@ export default function TeamManagementPage() {
|
|||
|
||||
{/* Tabs Content */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList className="bg-muted/50 p-1">
|
||||
<TabsTrigger value="members" className="gap-2 data-[state=active]:bg-background">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>Members</span>
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{members.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roles" className="gap-2 data-[state=active]:bg-background">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span>Roles</span>
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{roles.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="invites" className="gap-2 data-[state=active]:bg-background">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Invites</span>
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{invites.filter((i) => i.is_active).length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="overflow-x-auto pb-1 md:pb-0">
|
||||
<TabsList className="bg-muted/50 p-1 w-full md:w-fit grid grid-cols-3 md:flex">
|
||||
<TabsTrigger
|
||||
value="members"
|
||||
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
|
||||
>
|
||||
<Users className="h-4 w-4 hidden md:block" />
|
||||
<span>Members</span>
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{members.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="roles"
|
||||
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
|
||||
>
|
||||
<Shield className="h-4 w-4 hidden md:block" />
|
||||
<span>Roles</span>
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{roles.length}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<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 && (
|
||||
<CreateInviteDialog
|
||||
roles={roles}
|
||||
onCreateInvite={handleCreateInvite}
|
||||
searchSpaceId={searchSpaceId}
|
||||
className="w-full md:w-auto"
|
||||
/>
|
||||
)}
|
||||
{activeTab === "roles" && hasPermission("roles:create") && (
|
||||
<CreateRoleDialog
|
||||
groupedPermissions={groupedPermissions}
|
||||
onCreateRole={handleCreateRole}
|
||||
className="w-full md:w-auto"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -533,8 +530,6 @@ function MembersTab({
|
|||
canManageRoles: boolean;
|
||||
canRemove: boolean;
|
||||
}) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredMembers = useMemo(() => {
|
||||
|
|
@ -575,13 +570,13 @@ function MembersTab({
|
|||
</div>
|
||||
|
||||
{/* Members List */}
|
||||
<div className="rounded-lg border bg-card overflow-hidden">
|
||||
<div className="rounded-lg border bg-card overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[300px]">Member</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Joined</TableHead>
|
||||
<TableHead className="w-auto md:w-[300px] px-2 md:px-4">Member</TableHead>
|
||||
<TableHead className="px-2 md:px-4">Role</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Joined</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -604,11 +599,11 @@ function MembersTab({
|
|||
transition={{ delay: index * 0.05 }}
|
||||
className="group border-b transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
|
||||
<div className="flex items-center gap-1.5 md:gap-3">
|
||||
<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">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
<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-4 w-4 md:h-5 md:w-5 text-primary" />
|
||||
</div>
|
||||
{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">
|
||||
|
|
@ -616,12 +611,14 @@ function MembersTab({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{member.user_email || "Unknown"}</p>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-xs md:text-sm truncate">
|
||||
{member.user_email || "Unknown"}
|
||||
</p>
|
||||
{member.is_owner && (
|
||||
<Badge
|
||||
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
|
||||
</Badge>
|
||||
|
|
@ -629,7 +626,7 @@ function MembersTab({
|
|||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
|
||||
{canManageRoles && !member.is_owner ? (
|
||||
<Select
|
||||
value={member.role_id?.toString() || "none"}
|
||||
|
|
@ -637,7 +634,7 @@ function MembersTab({
|
|||
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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -653,19 +650,22 @@ function MembersTab({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Shield className="h-3 w-3" />
|
||||
<Badge
|
||||
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"}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{new Date(member.joined_at).toLocaleDateString()}
|
||||
</div>
|
||||
</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 && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
|
@ -962,11 +962,11 @@ function InvitesTab({
|
|||
className={cn("relative overflow-hidden transition-all", isInactive && "opacity-60")}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-start md:items-center gap-4 flex-1 min-w-0">
|
||||
<div
|
||||
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
|
||||
? "bg-emerald-500/20"
|
||||
: "bg-muted"
|
||||
|
|
@ -974,7 +974,7 @@ function InvitesTab({
|
|||
>
|
||||
<Link2
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
"h-5 w-5 md:h-6 md:w-6",
|
||||
invite.is_active && !isExpired && !isMaxedOut
|
||||
? "text-emerald-600"
|
||||
: "text-muted-foreground"
|
||||
|
|
@ -991,7 +991,7 @@ function InvitesTab({
|
|||
)}
|
||||
{isMaxedOut && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Max uses reached
|
||||
Maxed
|
||||
</Badge>
|
||||
)}
|
||||
{!invite.is_active && !isExpired && !isMaxedOut && (
|
||||
|
|
@ -1000,44 +1000,44 @@ function InvitesTab({
|
|||
</Badge>
|
||||
)}
|
||||
</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">
|
||||
<Shield className="h-3 w-3" />
|
||||
{invite.role?.name || "Default role"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Hash className="h-3 w-3" />
|
||||
{invite.uses_count} uses
|
||||
{invite.max_uses && ` / ${invite.max_uses}`}
|
||||
{invite.uses_count}
|
||||
{invite.max_uses ? ` / ${invite.max_uses} uses` : " uses"}
|
||||
</span>
|
||||
{invite.expires_at && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{isExpired
|
||||
? "Expired"
|
||||
: `Expires ${new Date(invite.expires_at).toLocaleDateString()}`}
|
||||
: `Exp: ${new Date(invite.expires_at).toLocaleDateString()}`}
|
||||
</span>
|
||||
)}
|
||||
</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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
className="gap-2 flex-1 md:flex-none"
|
||||
onClick={() => copyInviteLink(invite)}
|
||||
disabled={Boolean(isInactive)}
|
||||
>
|
||||
{copiedId === invite.id ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-emerald-500" />
|
||||
Copied!
|
||||
<span className="md:inline">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy Link
|
||||
<span className="md:inline">Copy</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -1088,11 +1088,11 @@ function InvitesTab({
|
|||
function CreateInviteDialog({
|
||||
roles,
|
||||
onCreateInvite,
|
||||
searchSpaceId,
|
||||
className,
|
||||
}: {
|
||||
roles: Role[];
|
||||
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
|
||||
searchSpaceId: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
|
@ -1142,12 +1142,12 @@ function CreateInviteDialog({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Button className={cn("gap-2", className)}>
|
||||
<UserPlus className="h-4 w-4" />
|
||||
Create Invite
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6">
|
||||
{createdInvite ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
|
|
@ -1159,7 +1159,7 @@ function CreateInviteDialog({
|
|||
Share this link to invite people to your search space.
|
||||
</DialogDescription>
|
||||
</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">
|
||||
<code className="flex-1 min-w-0 text-sm break-all">
|
||||
{window.location.origin}/invite/{createdInvite.invite_code}
|
||||
|
|
@ -1203,7 +1203,7 @@ function CreateInviteDialog({
|
|||
Create a link to invite people to this search space.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-3 py-2 md:py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-name">Name (optional)</Label>
|
||||
<Input
|
||||
|
|
@ -1234,7 +1234,7 @@ function CreateInviteDialog({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
</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">
|
||||
<Label htmlFor="max-uses">Max uses (optional)</Label>
|
||||
<Input
|
||||
|
|
@ -1301,9 +1301,11 @@ function CreateInviteDialog({
|
|||
function CreateRoleDialog({
|
||||
groupedPermissions,
|
||||
onCreateRole,
|
||||
className,
|
||||
}: {
|
||||
groupedPermissions: Record<string, { value: string; name: string; category: string }[]>;
|
||||
onCreateRole: (data: CreateRoleRequest["data"]) => Promise<Role>;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
|
@ -1358,20 +1360,20 @@ function CreateRoleDialog({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Button className={cn("gap-2", className)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Role
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-xl p-4 md:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Custom Role</DialogTitle>
|
||||
<DialogDescription>
|
||||
<DialogDescription className="text-xs md:text-sm">
|
||||
Define a new role with specific permissions for this search space.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-3 py-2 md:py-4">
|
||||
<div className="flex flex-col md:grid md:grid-cols-2 gap-3 md:gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role-name">Role Name *</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||
|
|
@ -38,7 +37,6 @@ import {
|
|||
} from "@/components/ui/card";
|
||||
import { Spotlight } from "@/components/ui/spotlight";
|
||||
import { Tilt } from "@/components/ui/tilt";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
|
||||
/**
|
||||
* Formats a date string into a readable format
|
||||
|
|
@ -65,7 +63,7 @@ const LoadingScreen = () => {
|
|||
animate={{ opacity: 1, scale: 1 }}
|
||||
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">
|
||||
<CardTitle className="text-xl font-medium">{t("loading")}</CardTitle>
|
||||
<CardDescription>{t("fetching_spaces")}</CardDescription>
|
||||
|
|
@ -101,7 +99,7 @@ const ErrorScreen = ({ message }: { message: string }) => {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
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">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
|
|
@ -185,21 +183,21 @@ const DashboardPage = () => {
|
|||
|
||||
return (
|
||||
<motion.div
|
||||
className="container mx-auto py-10"
|
||||
className="container mx-auto py-6 md:py-10 px-4"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className="flex flex-col space-y-6" variants={itemVariants}>
|
||||
<div className="flex flex-row space-x-4 justify-between">
|
||||
<div className="flex flex-row space-x-4">
|
||||
<Logo className="w-10 h-10 rounded-md" />
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h1 className="text-4xl font-bold">{t("surfsense_dashboard")}</h1>
|
||||
<p className="text-muted-foreground">{t("welcome_message")}</p>
|
||||
<motion.div className="flex flex-col space-y-4 md:space-y-6" variants={itemVariants}>
|
||||
<div className="flex flex-row items-center justify-between gap-2">
|
||||
<div className="flex flex-row items-center md:space-x-4">
|
||||
<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-0.5 md:space-y-2">
|
||||
<h1 className="text-xl md:text-4xl font-bold">{t("surfsense_dashboard")}</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground">{t("welcome_message")}</p>
|
||||
</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} />
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
|
|
@ -207,18 +205,18 @@ const DashboardPage = () => {
|
|||
|
||||
<div className="flex flex-col space-y-6 mt-6">
|
||||
<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 }}>
|
||||
<Link href="/dashboard/searchspaces">
|
||||
<Button className="h-10">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Button className="h-8 md:h-10 text-[11px] md:text-sm px-3 md:px-4">
|
||||
<Plus className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
|
||||
{t("create_search_space")}
|
||||
</Button>
|
||||
</Link>
|
||||
</motion.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.length > 0 &&
|
||||
searchSpaces.map((space) => (
|
||||
|
|
@ -295,14 +293,17 @@ const DashboardPage = () => {
|
|||
<div className="flex flex-1 flex-col justify-between p-1">
|
||||
<div>
|
||||
<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 && (
|
||||
<Badge variant="secondary" className="text-xs font-normal">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] md:text-xs font-normal"
|
||||
>
|
||||
{t("shared")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-1 text-xs md:text-sm text-muted-foreground">
|
||||
{space.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -334,8 +335,10 @@ const DashboardPage = () => {
|
|||
<div className="rounded-full bg-muted/50 p-4 mb-4">
|
||||
<Search className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">{t("no_spaces_found")}</h3>
|
||||
<p className="text-muted-foreground mb-6">{t("create_first_space")}</p>
|
||||
<h3 className="text-base md:text-lg font-medium mb-2">{t("no_spaces_found")}</h3>
|
||||
<p className="text-xs md:text-sm text-muted-foreground mb-6">
|
||||
{t("create_first_space")}
|
||||
</p>
|
||||
<Link href="/dashboard/searchspaces">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
|
|
@ -359,8 +362,10 @@ const DashboardPage = () => {
|
|||
>
|
||||
<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">
|
||||
<Plus className="h-10 w-10 mb-3 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{t("add_new_search_space")}</span>
|
||||
<Plus className="h-8 w-8 md:h-10 md:w-10 mb-2 md:mb-3 text-muted-foreground" />
|
||||
<span className="text-xs md:text-sm font-medium">
|
||||
{t("add_new_search_space")}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</Tilt>
|
||||
|
|
|
|||
|
|
@ -159,3 +159,4 @@ button {
|
|||
|
||||
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
|
||||
@source '../node_modules/streamdown/dist/*.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -162,5 +162,52 @@ export default function BlockNoteEditor({
|
|||
}, [resolvedTheme]);
|
||||
|
||||
// Renders the editor instance
|
||||
return <BlockNoteView editor={editor} theme={blockNoteTheme} />;
|
||||
return (
|
||||
<div className="bn-container">
|
||||
<style>{`
|
||||
@media (max-width: 640px) {
|
||||
.bn-container .bn-editor {
|
||||
padding-inline: 12px !important;
|
||||
}
|
||||
|
||||
/* Heading Level 1 (Title) */
|
||||
.bn-container [data-content-type="heading"][data-level="1"] {
|
||||
font-size: 1.75rem !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Heading Level 2 */
|
||||
.bn-container [data-content-type="heading"][data-level="2"] {
|
||||
font-size: 1.5rem !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-top: 0.875rem !important;
|
||||
margin-bottom: 0.375rem !important;
|
||||
}
|
||||
|
||||
/* Heading Level 3 */
|
||||
.bn-container [data-content-type="heading"][data-level="3"] {
|
||||
font-size: 1.25rem !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-top: 0.75rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
|
||||
/* Paragraphs and regular content */
|
||||
.bn-container .bn-block-content {
|
||||
font-size: 0.9375rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
/* Adjust lists */
|
||||
.bn-container ul,
|
||||
.bn-container ol {
|
||||
padding-left: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<BlockNoteView editor={editor} theme={blockNoteTheme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export function LanguageSwitcher() {
|
|||
|
||||
return (
|
||||
<Select value={locale} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<Globe className="mr-2 h-4 w-4" />
|
||||
<SelectTrigger className="w-[110px] sm:w-[160px] h-8 sm:h-10 text-xs sm:text-sm px-2 sm:px-3 gap-1 sm:gap-2">
|
||||
<Globe className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<SelectValue>
|
||||
{languages.find((lang) => lang.code === locale)?.name || "English"}
|
||||
</SelectValue>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { BadgeCheck, LogOut, Settings } from "lucide-react";
|
||||
import { BadgeCheck, LogOut } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -51,23 +51,28 @@ export function UserDropdown({
|
|||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<DropdownMenuContent className="w-44 md:w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal p-2 md:p-3">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{user.name}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
||||
<p className="text-xs md:text-sm font-medium leading-none">{user.name}</p>
|
||||
<p className="text-[10px] md:text-xs leading-none text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => router.push(`/dashboard/api-key`)}>
|
||||
<BadgeCheck className="mr-2 h-4 w-4" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push(`/dashboard/api-key`)}
|
||||
className="text-xs md:text-sm"
|
||||
>
|
||||
<BadgeCheck className="mr-2 h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
API Key
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<DropdownMenuItem onClick={handleLogout} className="text-xs md:text-sm">
|
||||
<LogOut className="mr-2 h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
|
|||
"text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{/* Header text with shimmer if processing or has in-progress step */}
|
||||
{isProcessing || inProgressStep ? (
|
||||
{/* Header text with shimmer if processing (streaming) */}
|
||||
{isProcessing ? (
|
||||
<TextShimmerLoader text={getHeaderText()} size="sm" />
|
||||
) : (
|
||||
<span>{getHeaderText()}</span>
|
||||
|
|
@ -398,7 +398,7 @@ const ThreadWelcome: FC = () => {
|
|||
<div className="aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative">
|
||||
{/* Greeting positioned above the composer - fixed position */}
|
||||
<div className="aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center">
|
||||
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-5xl delay-100 duration-500 ease-out fill-mode-both">
|
||||
<h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-3xl md:text-5xl delay-100 duration-500 ease-out fill-mode-both">
|
||||
{greeting}
|
||||
</h1>
|
||||
</div>
|
||||
|
|
@ -891,14 +891,17 @@ const ThinkingStepsPart: FC = () => {
|
|||
const messageId = useAssistantState(({ message }) => message?.id);
|
||||
const thinkingSteps = thinkingStepsMap.get(messageId) || [];
|
||||
|
||||
// Check if thread is still running (for stopping the spinner when cancelled)
|
||||
// Check if this specific message is currently streaming
|
||||
// A message is streaming if: thread is running AND this is the last assistant message
|
||||
const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning);
|
||||
const isLastMessage = useAssistantState(({ message }) => message?.isLast ?? false);
|
||||
const isMessageStreaming = isThreadRunning && isLastMessage;
|
||||
|
||||
if (thinkingSteps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isThreadRunning} />
|
||||
<ThinkingStepsDisplay steps={thinkingSteps} isThreadRunning={isMessageStreaming} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
|
||||
import { useGithubStars } from "@/hooks/use-github-stars";
|
||||
|
|
@ -118,89 +118,88 @@ const MobileNav = ({ navItems, isScrolled }: any) => {
|
|||
const { compactFormat: githubStars, loading: loadingGithubStars } = useGithubStars();
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
animate={{ borderRadius: open ? "4px" : "2rem" }}
|
||||
key={String(open)}
|
||||
className={cn(
|
||||
"mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between px-4 py-2 lg:hidden transition-all duration-300",
|
||||
isScrolled
|
||||
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
: "bg-transparent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="relative z-50 flex items-center justify-center p-2 -mr-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
>
|
||||
{open ? (
|
||||
<IconX className="h-6 w-6 text-black dark:text-white" />
|
||||
) : (
|
||||
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
|
||||
)}
|
||||
</button>
|
||||
<motion.div
|
||||
animate={{ borderRadius: open ? "4px" : "2rem" }}
|
||||
key={String(open)}
|
||||
className={cn(
|
||||
"relative mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between px-4 py-2 lg:hidden transition-all duration-300",
|
||||
isScrolled
|
||||
? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
: "bg-transparent border border-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Logo className="h-8 w-8 rounded-md" />
|
||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-x-0 top-16 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white/80 backdrop-blur-md border border-white/20 shadow-lg px-4 py-8 dark:bg-neutral-950/80 dark:border-neutral-800/50"
|
||||
>
|
||||
{navItems.map((navItem: any, idx: number) => (
|
||||
<Link
|
||||
key={`link=${idx}`}
|
||||
href={navItem.link}
|
||||
className="relative text-neutral-600 dark:text-neutral-300"
|
||||
>
|
||||
<motion.span className="block">{navItem.name} </motion.span>
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex w-full items-center gap-2 pt-2">
|
||||
<Link
|
||||
href="https://discord.gg/ejRNvftDp9"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
>
|
||||
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 rounded-lg px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
>
|
||||
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
{loadingGithubStars ? (
|
||||
<div className="w-6 h-5 dark:bg-neutral-800 animate-pulse"></div>
|
||||
) : (
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
||||
{githubStars}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</motion.div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="relative z-50 flex items-center justify-center p-2 -mr-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
>
|
||||
{open ? (
|
||||
<IconX className="h-6 w-6 text-black dark:text-white" />
|
||||
) : (
|
||||
<IconMenu2 className="h-6 w-6 text-black dark:text-white" />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="absolute inset-x-0 top-full mt-1 z-20 flex w-full flex-col items-start justify-start gap-4 rounded-xl bg-white/90 backdrop-blur-xl border border-white/20 shadow-2xl px-4 py-6 dark:bg-neutral-950/90 dark:border-neutral-800/50"
|
||||
>
|
||||
{navItems.map((navItem: any, idx: number) => (
|
||||
<Link
|
||||
key={`link=${idx}`}
|
||||
href={navItem.link}
|
||||
className="relative text-neutral-600 dark:text-neutral-300"
|
||||
>
|
||||
<motion.span className="block">{navItem.name} </motion.span>
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex w-full items-center gap-2 pt-2">
|
||||
<Link
|
||||
href="https://discord.gg/ejRNvftDp9"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
>
|
||||
<IconBrandDiscord className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/MODSetter/SurfSense"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 rounded-lg px-3 py-2 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors touch-manipulation"
|
||||
>
|
||||
<IconBrandGithub className="h-5 w-5 text-neutral-600 dark:text-neutral-300" />
|
||||
{loadingGithubStars ? (
|
||||
<div className="w-6 h-5 dark:bg-neutral-800 animate-pulse"></div>
|
||||
) : (
|
||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
||||
{githubStars}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-full rounded-lg bg-black px-8 py-2 font-medium text-white shadow-[0px_-2px_0px_0px_rgba(255,255,255,0.4)_inset] dark:bg-white dark:text-black text-center touch-manipulation"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
|
|||
<div className="space-y-6 p-2 sm:p-0">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr_auto] md:gap-3 items-end">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Label htmlFor="param-key" className="text-sm font-medium">
|
||||
<Label htmlFor="param-key" className="text-xs sm:text-sm font-medium">
|
||||
Parameter Key
|
||||
</Label>
|
||||
<Select value={selectedKey} onValueChange={setSelectedKey}>
|
||||
|
|
@ -87,7 +87,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Label htmlFor="param-value" className="text-sm font-medium">
|
||||
<Label htmlFor="param-value" className="text-xs sm:text-sm font-medium">
|
||||
Value
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -100,11 +100,11 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa
|
|||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full md:w-auto h-10 mt-0"
|
||||
className="w-full md:w-auto h-9 sm:h-10 mt-0 text-xs sm:text-sm"
|
||||
onClick={handleAdd}
|
||||
disabled={!selectedKey || value === ""}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" /> Add Parameter
|
||||
<Plus className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" /> Add Parameter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,13 @@ export function JsonMetadataViewer({
|
|||
if (open !== undefined && onOpenChange !== undefined) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title} - Metadata</DialogTitle>
|
||||
<DialogTitle className="text-base sm:text-lg truncate pr-6">
|
||||
{title} - Metadata
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 p-4 bg-muted/30 rounded-md">
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||
<JsonView data={jsonData} style={defaultStyles} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
@ -70,11 +72,13 @@ export function JsonMetadataViewer({
|
|||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogContent className="sm:max-w-4xl max-w-[95vw] w-full max-h-[80vh] overflow-y-auto p-4 sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title} - Metadata</DialogTitle>
|
||||
<DialogTitle className="text-base sm:text-lg truncate pr-6">
|
||||
{title} - Metadata
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 p-4 bg-muted/30 rounded-md">
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||
<JsonView data={jsonData} style={defaultStyles} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -184,8 +184,8 @@ export const DocumentMentionPicker = forwardRef<
|
|||
role="listbox"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{/* Document List */}
|
||||
<div className="max-h-[280px] overflow-y-auto">
|
||||
{/* Document List - Shows max 3 items on mobile, 5 items on desktop */}
|
||||
<div className="max-h-[108px] sm:max-h-[180px] overflow-y-auto">
|
||||
{actualLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="animate-spin h-5 w-5 border-2 border-primary border-t-transparent rounded-full" />
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ export function ModelConfigSidebar({
|
|||
<Bot className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{getTitle()}</h2>
|
||||
<h2 className="text-base sm:text-lg font-semibold">{getTitle()}</h2>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{isGlobal ? (
|
||||
<Badge variant="secondary" className="gap-1 text-xs">
|
||||
|
|
@ -207,9 +207,10 @@ export function ModelConfigSidebar({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="rounded-xl hover:bg-destructive/10 hover:text-destructive"
|
||||
className="h-8 w-8 rounded-full"
|
||||
>
|
||||
<X className="size-5" />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -175,39 +175,44 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
|||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"h-9 gap-2 px-3 rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
|
||||
"h-7 md:h-9 gap-1 md:gap-2 px-2 md:px-3 rounded-lg md:rounded-xl border border-border/80 bg-background/50 backdrop-blur-sm",
|
||||
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
|
||||
"text-sm font-medium text-foreground",
|
||||
"text-xs md:text-sm font-medium text-foreground",
|
||||
"focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Loading...</span>
|
||||
<Loader2 className="size-3.5 md:size-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-muted-foreground hidden md:inline">Loading...</span>
|
||||
<span className="text-muted-foreground md:hidden">Load...</span>
|
||||
</>
|
||||
) : currentConfig ? (
|
||||
<>
|
||||
{getProviderIcon(currentConfig.provider)}
|
||||
<span className="max-w-[150px] truncate">{currentConfig.name}</span>
|
||||
<Badge variant="secondary" className="ml-1 text-[10px] px-1.5 py-0 h-4 bg-muted/80">
|
||||
{currentConfig.model_name.split("/").pop()?.slice(0, 15) ||
|
||||
currentConfig.model_name.slice(0, 15)}
|
||||
<span className="max-w-[80px] md:max-w-[150px] truncate">{currentConfig.name}</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="ml-0.5 md:ml-1 text-[9px] md:text-[10px] px-1 md:px-1.5 py-0 h-3.5 md:h-4 bg-muted/80"
|
||||
>
|
||||
{currentConfig.model_name.split("/").pop()?.slice(0, 10) ||
|
||||
currentConfig.model_name.slice(0, 10)}
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Bot className="size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Select Model</span>
|
||||
<Bot className="size-3.5 md:size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground hidden md:inline">Select Model</span>
|
||||
<span className="text-muted-foreground md:hidden">Model</span>
|
||||
</>
|
||||
)}
|
||||
<ChevronDown className="size-3.5 text-muted-foreground ml-1 shrink-0" />
|
||||
<ChevronDown className="size-3 md:size-3.5 text-muted-foreground ml-1 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[360px] p-0 rounded-xl shadow-lg border-border/30"
|
||||
className="w-[280px] md:w-[360px] p-0 rounded-lg md:rounded-xl shadow-lg border-border/30"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
>
|
||||
|
|
@ -225,17 +230,17 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
|
||||
<div className="flex items-center gap-1 md:gap-2 border-b border-border/30 px-2 md:px-3 py-1.5 md:py-2">
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
className="h-8 border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
|
||||
className="h-7 md:h-8 text-xs md:text-sm border-0 bg-transparent focus:ring-0 placeholder:text-muted-foreground/60"
|
||||
disabled={isSwitching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CommandList className="max-h-[400px] overflow-y-auto">
|
||||
<CommandList className="max-h-[300px] md:max-h-[400px] overflow-y-auto">
|
||||
<CommandEmpty className="py-8 text-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Bot className="size-8 text-muted-foreground/40" />
|
||||
|
|
|
|||
|
|
@ -352,9 +352,9 @@ export function SourceDetailPanel({
|
|||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="rounded-xl h-10 w-10 hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
className="h-8 w-8 rounded-full"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
const hasError = configsError || preferencesError || globalConfigsError;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -179,9 +179,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
size="sm"
|
||||
onClick={() => refreshConfigs()}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${configsLoading ? "animate-spin" : ""}`} />
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 md:h-4 md:w-4 ${configsLoading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span className="hidden sm:inline">Refresh Configs</span>
|
||||
<span className="sm:hidden">Configs</span>
|
||||
</Button>
|
||||
|
|
@ -190,9 +192,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
size="sm"
|
||||
onClick={() => refreshPreferences()}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${preferencesLoading ? "animate-spin" : ""}`} />
|
||||
<RefreshCw
|
||||
className={`h-3 w-3 md:h-4 md:w-4 ${preferencesLoading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span className="hidden sm:inline">Refresh Preferences</span>
|
||||
<span className="sm:hidden">Prefs</span>
|
||||
</Button>
|
||||
|
|
@ -201,9 +205,9 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
|
||||
{/* Error Alert */}
|
||||
{hasError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert variant="destructive" className="py-3 md:py-4">
|
||||
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
{(configsError?.message ?? "Failed to load LLM configurations") ||
|
||||
(preferencesError?.message ?? "Failed to load preferences") ||
|
||||
(globalConfigsError?.message ?? "Failed to load global configurations")}
|
||||
|
|
@ -214,10 +218,10 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<CardContent className="flex items-center justify-center py-8 md:py-12">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>
|
||||
<Loader2 className="w-4 h-4 md:w-5 md:h-5 animate-spin" />
|
||||
<span className="text-xs md:text-sm">
|
||||
{configsLoading && preferencesLoading
|
||||
? "Loading configurations and preferences..."
|
||||
: configsLoading
|
||||
|
|
@ -231,27 +235,27 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
|
||||
{/* Info Alert */}
|
||||
{!isLoading && !hasError && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{availableConfigs.length === 0 ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert variant="destructive" className="py-3 md:py-4">
|
||||
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
No LLM configurations found. Please add at least one LLM provider in the Agent
|
||||
Configs tab before assigning roles.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : !isAssignmentComplete ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert className="py-3 md:py-4">
|
||||
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
Complete all role assignments to enable full functionality. Each role serves
|
||||
different purposes in your workflow.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert className="py-3 md:py-4">
|
||||
<CheckCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
All roles are assigned and ready to use! Your LLM configuration is complete.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
@ -259,7 +263,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
|
||||
{/* Role Assignment Cards */}
|
||||
{availableConfigs.length > 0 && (
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-4 md:gap-6">
|
||||
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
|
||||
const IconComponent = role.icon;
|
||||
const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
|
||||
|
|
@ -277,28 +281,34 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
<Card
|
||||
className={`border-l-4 ${currentAssignment ? "border-l-primary" : "border-l-muted"} hover:shadow-md transition-shadow`}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-2 md:pb-3 px-3 md:px-6 pt-3 md:pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${role.color}`}>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className={`p-1.5 md:p-2 rounded-lg ${role.color}`}>
|
||||
<IconComponent className="w-4 h-4 md:w-5 md:h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{role.title}</CardTitle>
|
||||
<CardDescription className="mt-1">{role.description}</CardDescription>
|
||||
<CardTitle className="text-base md:text-lg">{role.title}</CardTitle>
|
||||
<CardDescription className="mt-0.5 md:mt-1 text-xs md:text-sm">
|
||||
{role.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
{currentAssignment && <CheckCircle className="w-5 h-5 text-green-500" />}
|
||||
{currentAssignment && (
|
||||
<CheckCircle className="w-4 h-4 md:w-5 md:h-5 text-green-500 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Assign LLM Configuration:</Label>
|
||||
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
|
||||
<div className="space-y-1.5 md:space-y-2">
|
||||
<Label className="text-xs md:text-sm font-medium">
|
||||
Assign LLM Configuration:
|
||||
</Label>
|
||||
<Select
|
||||
value={currentAssignment?.toString() || "unassigned"}
|
||||
onValueChange={(value) => handleRoleAssignment(`${key}_llm_id`, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-9 md:h-10 text-xs md:text-sm">
|
||||
<SelectValue placeholder="Select an LLM configuration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -361,23 +371,25 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
</div>
|
||||
|
||||
{assignedConfig && (
|
||||
<div className="mt-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm flex-wrap">
|
||||
<Bot className="w-4 h-4" />
|
||||
<div className="mt-2 md:mt-3 p-2 md:p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-1.5 md:gap-2 text-xs md:text-sm flex-wrap">
|
||||
<Bot className="w-3 h-3 md:w-4 md:h-4 shrink-0" />
|
||||
<span className="font-medium">Assigned:</span>
|
||||
<Badge variant="secondary">{assignedConfig.provider}</Badge>
|
||||
<Badge variant="secondary" className="text-[10px] md:text-xs">
|
||||
{assignedConfig.provider}
|
||||
</Badge>
|
||||
<span>{assignedConfig.name}</span>
|
||||
{"is_global" in assignedConfig && assignedConfig.is_global && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Badge variant="outline" className="text-[9px] md:text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
<div className="text-[10px] md:text-xs text-muted-foreground mt-0.5 md:mt-1">
|
||||
Model: {assignedConfig.model_name}
|
||||
</div>
|
||||
{assignedConfig.api_base && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="text-[10px] md:text-xs text-muted-foreground">
|
||||
Base: {assignedConfig.api_base}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -393,18 +405,22 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
|
|||
|
||||
{/* Action Buttons */}
|
||||
{hasChanges && (
|
||||
<div className="flex justify-center gap-3 pt-4">
|
||||
<Button onClick={handleSave} disabled={isSaving} className="flex items-center gap-2">
|
||||
<Save className="w-4 h-4" />
|
||||
<div className="flex justify-center gap-2 md:gap-3 pt-3 md:pt-4">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<RotateCcw className="w-3.5 h-3.5 md:w-4 md:h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
LLM_PROVIDERS.find((p) => p.value === providerValue);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -169,9 +169,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
size="sm"
|
||||
onClick={() => refreshConfigs()}
|
||||
disabled={isLoading}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
|
||||
>
|
||||
<RefreshCw className={cn("h-4 w-4", isLoading && "animate-spin")} />
|
||||
<RefreshCw className={cn("h-3 w-3 md:h-4 md:w-4", isLoading && "animate-spin")} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -187,9 +187,11 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{err?.message ?? "Something went wrong"}</AlertDescription>
|
||||
<Alert variant="destructive" className="py-3 md:py-4">
|
||||
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
{err?.message ?? "Something went wrong"}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</motion.div>
|
||||
))}
|
||||
|
|
@ -198,9 +200,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
{/* Global Configs Info */}
|
||||
{globalConfigs.length > 0 && (
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<Alert className="border-blue-500/30 bg-blue-500/5">
|
||||
<Sparkles className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-200">
|
||||
<Alert className="border-blue-500/30 bg-blue-500/5 py-3 md:py-4">
|
||||
<Sparkles className="h-3 w-3 md:h-4 md:w-4 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-200 text-xs md:text-sm">
|
||||
<span className="font-medium">{globalConfigs.length} global configuration(s)</span>{" "}
|
||||
available from your administrator. These are pre-configured and ready to use.{" "}
|
||||
<span className="text-blue-600 dark:text-blue-300">
|
||||
|
|
@ -214,10 +216,12 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
{/* Loading State */}
|
||||
{isLoading && (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-16">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Loading configurations...</span>
|
||||
<CardContent className="flex items-center justify-center py-10 md:py-16">
|
||||
<div className="flex flex-col items-center gap-2 md:gap-3">
|
||||
<Loader2 className="h-6 w-6 md:h-8 md:w-8 animate-spin text-muted-foreground" />
|
||||
<span className="text-xs md:text-sm text-muted-foreground">
|
||||
Loading configurations...
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -225,11 +229,14 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
|
||||
{/* Configurations List */}
|
||||
{!isLoading && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
|
||||
<h3 className="text-xl font-semibold tracking-tight">Your Configurations</h3>
|
||||
<Button onClick={openNewDialog} className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<h3 className="text-lg md:text-xl font-semibold tracking-tight">Your Configurations</h3>
|
||||
<Button
|
||||
onClick={openNewDialog}
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
|
||||
>
|
||||
<Plus className="h-3 w-3 md:h-4 md:w-4" />
|
||||
Add Configuration
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -237,18 +244,22 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
{configs?.length === 0 ? (
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
|
||||
<Card className="border-dashed border-2 border-muted-foreground/25">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="rounded-full bg-gradient-to-br from-violet-500/10 to-purple-500/10 p-6 mb-6">
|
||||
<Wand2 className="h-12 w-12 text-violet-600 dark:text-violet-400" />
|
||||
<CardContent className="flex flex-col items-center justify-center py-10 md:py-16 text-center">
|
||||
<div className="rounded-full bg-gradient-to-br from-violet-500/10 to-purple-500/10 p-4 md:p-6 mb-4 md:mb-6">
|
||||
<Wand2 className="h-8 w-8 md:h-12 md:w-12 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<div className="space-y-2 mb-6">
|
||||
<h3 className="text-xl font-semibold">No Configurations Yet</h3>
|
||||
<p className="text-muted-foreground max-w-sm">
|
||||
<div className="space-y-2 mb-4 md:mb-6">
|
||||
<h3 className="text-lg md:text-xl font-semibold">No Configurations Yet</h3>
|
||||
<p className="text-xs md:text-sm text-muted-foreground max-w-sm">
|
||||
Create your first AI configuration to customize how your agent responds
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={openNewDialog} size="lg" className="gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<Button
|
||||
onClick={openNewDialog}
|
||||
size="lg"
|
||||
className="gap-2 text-xs md:text-sm h-9 md:h-10"
|
||||
>
|
||||
<Plus className="h-3 w-3 md:h-4 md:w-4" />
|
||||
Create First Configuration
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
|
@ -270,25 +281,25 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
<CardContent className="p-0">
|
||||
<div className="flex">
|
||||
{/* Left accent bar */}
|
||||
<div className="w-1.5 transition-colors bg-gradient-to-b from-violet-500/50 to-purple-500/50 group-hover:from-violet-500 group-hover:to-purple-500" />
|
||||
<div className="w-1 md:w-1.5 transition-colors bg-gradient-to-b from-violet-500/50 to-purple-500/50 group-hover:from-violet-500 group-hover:to-purple-500" />
|
||||
|
||||
<div className="flex-1 p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 p-3 md:p-5">
|
||||
<div className="flex items-start justify-between gap-2 md:gap-4">
|
||||
{/* Main content */}
|
||||
<div className="flex items-start gap-4 flex-1 min-w-0">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500/10 to-purple-500/10 group-hover:from-violet-500/20 group-hover:to-purple-500/20 transition-colors">
|
||||
<Bot className="h-6 w-6 text-violet-600 dark:text-violet-400" />
|
||||
<div className="flex items-start gap-2 md:gap-4 flex-1 min-w-0">
|
||||
<div className="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-lg md:rounded-xl bg-gradient-to-br from-violet-500/10 to-purple-500/10 group-hover:from-violet-500/20 group-hover:to-purple-500/20 transition-colors shrink-0">
|
||||
<Bot className="h-5 w-5 md:h-6 md:w-6 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 space-y-3">
|
||||
<div className="flex-1 min-w-0 space-y-2 md:space-y-3">
|
||||
{/* Title row */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="text-base font-semibold tracking-tight truncate">
|
||||
<div className="flex items-center gap-1.5 md:gap-2 flex-wrap">
|
||||
<h4 className="text-sm md:text-base font-semibold tracking-tight truncate">
|
||||
{config.name}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<div className="flex items-center gap-1 md:gap-1.5 flex-wrap">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] font-medium px-2 py-0.5 bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/20"
|
||||
className="text-[9px] md:text-[10px] font-medium px-1.5 md:px-2 py-0.5 bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/20"
|
||||
>
|
||||
{config.provider}
|
||||
</Badge>
|
||||
|
|
@ -298,9 +309,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
<TooltipTrigger>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-2 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300"
|
||||
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300"
|
||||
>
|
||||
<MessageSquareQuote className="h-3 w-3 mr-1" />
|
||||
<MessageSquareQuote className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
|
||||
Citations
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -317,9 +328,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
<TooltipTrigger>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-2 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300"
|
||||
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
<FileText className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
|
||||
Custom
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -333,21 +344,21 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
</div>
|
||||
|
||||
{/* Model name */}
|
||||
<code className="text-xs font-mono text-muted-foreground bg-muted/50 px-2 py-1 rounded-md inline-block">
|
||||
<code className="text-[10px] md:text-xs font-mono text-muted-foreground bg-muted/50 px-1.5 md:px-2 py-0.5 md:py-1 rounded-md inline-block">
|
||||
{config.model_name}
|
||||
</code>
|
||||
|
||||
{/* Description if any */}
|
||||
{config.description && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-1">
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground line-clamp-1">
|
||||
{config.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Footer row */}
|
||||
<div className="flex items-center gap-4 pt-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<div className="flex items-center gap-2 md:gap-4 pt-1">
|
||||
<div className="flex items-center gap-1 md:gap-1.5 text-[10px] md:text-xs text-muted-foreground">
|
||||
<Clock className="h-2.5 w-2.5 md:h-3 md:w-3" />
|
||||
<span>
|
||||
{new Date(config.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
|
|
@ -357,7 +368,7 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center gap-0.5 md:gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -365,9 +376,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => openEditDialog(config)}
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
<Edit3 className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Edit</TooltipContent>
|
||||
|
|
@ -380,9 +391,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfigToDelete(config)}
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
|
||||
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Trash2 className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
|
|
|
|||
|
|
@ -92,15 +92,15 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-4 w-full max-w-md" />
|
||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
|
||||
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
|
||||
<Skeleton className="h-16 md:h-20 w-full" />
|
||||
<Skeleton className="h-24 md:h-32 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -108,23 +108,23 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Work in Progress Notice */}
|
||||
<Alert
|
||||
variant="default"
|
||||
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700"
|
||||
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700 py-3 md:py-4"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-500" />
|
||||
<AlertDescription className="text-amber-800 dark:text-amber-300">
|
||||
<AlertTriangle className="h-3 w-3 md:h-4 md:w-4 text-amber-600 dark:text-amber-500 shrink-0" />
|
||||
<AlertDescription className="text-amber-800 dark:text-amber-300 text-xs md:text-sm">
|
||||
<span className="font-semibold">Work in Progress:</span> This functionality is currently
|
||||
under development and not yet connected to the backend. Your instructions will be saved
|
||||
but won't affect AI behavior until the feature is fully implemented.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert className="py-3 md:py-4">
|
||||
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
System instructions apply to all AI interactions in this search space. They guide how the
|
||||
AI responds, its tone, focus areas, and behavior patterns.
|
||||
</AlertDescription>
|
||||
|
|
@ -132,16 +132,19 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
|
||||
{/* System Instructions Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Custom System Instructions</CardTitle>
|
||||
<CardDescription>
|
||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||
<CardTitle className="text-base md:text-lg">Custom System Instructions</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Provide specific guidelines for how you want the AI to respond. These instructions will
|
||||
be applied to all answers in this search space.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-instructions-settings" className="text-base font-medium">
|
||||
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
|
||||
<div className="space-y-1.5 md:space-y-2">
|
||||
<Label
|
||||
htmlFor="custom-instructions-settings"
|
||||
className="text-sm md:text-base font-medium"
|
||||
>
|
||||
Your Instructions
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -149,11 +152,11 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
placeholder="E.g., Always provide practical examples, be concise, focus on technical details, use simple language, respond in a specific format..."
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
rows={12}
|
||||
className="resize-none font-mono text-sm"
|
||||
rows={10}
|
||||
className="resize-none font-mono text-xs md:text-sm"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-[10px] md:text-xs text-muted-foreground">
|
||||
{customInstructions.length} characters
|
||||
</p>
|
||||
{customInstructions.length > 0 && (
|
||||
|
|
@ -161,7 +164,7 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setCustomInstructions("")}
|
||||
className="h-auto py-1 px-2 text-xs"
|
||||
className="h-auto py-0.5 md:py-1 px-1.5 md:px-2 text-[10px] md:text-xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
|
|
@ -170,9 +173,9 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
</div>
|
||||
|
||||
{customInstructions.trim().length === 0 && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<Alert className="py-2 md:py-3">
|
||||
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
|
||||
<AlertDescription className="text-xs md:text-sm">
|
||||
No system instructions are currently set. The AI will use default behavior.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
@ -181,22 +184,22 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div className="flex items-center justify-between pt-3 md:pt-4 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges || saving}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
<RotateCcw className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
Reset Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center gap-2 text-xs md:text-sm h-9 md:h-10"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
{saving ? "Saving..." : "Save Instructions"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -204,10 +207,10 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
|
|||
{hasChanges && (
|
||||
<Alert
|
||||
variant="default"
|
||||
className="bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800"
|
||||
className="bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800 py-3 md:py-4"
|
||||
>
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-500" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-300">
|
||||
<Info className="h-3 w-3 md:h-4 md:w-4 text-blue-600 dark:text-blue-500 shrink-0" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-300 text-xs md:text-sm">
|
||||
You have unsaved changes. Click "Save Instructions" to apply them.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -156,8 +156,8 @@ export function LLMConfigForm({
|
|||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className="space-y-6">
|
||||
{/* System Instructions & Citations Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<MessageSquareQuote className="h-4 w-4" />
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
|
||||
<MessageSquareQuote className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
System Instructions
|
||||
</div>
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ export function LLMConfigForm({
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Instructions for the AI</FormLabel>
|
||||
<FormLabel className="text-xs sm:text-sm">Instructions for the AI</FormLabel>
|
||||
{defaultInstructions && (
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -177,7 +177,7 @@ export function LLMConfigForm({
|
|||
onClick={() =>
|
||||
field.onChange(defaultInstructions.default_system_instructions)
|
||||
}
|
||||
className="h-7 text-xs text-muted-foreground hover:text-foreground"
|
||||
className="h-7 text-[10px] sm:text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
|
|
@ -187,11 +187,11 @@ export function LLMConfigForm({
|
|||
<Textarea
|
||||
placeholder="Enter system instructions for the AI..."
|
||||
rows={6}
|
||||
className="font-mono text-xs resize-none"
|
||||
className="font-mono text-[11px] sm:text-xs resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-xs">
|
||||
<FormDescription className="text-[10px] sm:text-xs">
|
||||
Use {"{resolved_today}"} to include today's date dynamically
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
|
|
@ -206,8 +206,8 @@ export function LLMConfigForm({
|
|||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border p-3 bg-muted/30">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-sm font-medium">Enable Citations</FormLabel>
|
||||
<FormDescription className="text-xs">
|
||||
<FormLabel className="text-xs sm:text-sm font-medium">Enable Citations</FormLabel>
|
||||
<FormDescription className="text-[10px] sm:text-xs">
|
||||
Include [citation:id] references to source documents
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
|
@ -223,8 +223,8 @@ export function LLMConfigForm({
|
|||
|
||||
{/* Model Configuration Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<Bot className="h-4 w-4" />
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
|
||||
<Bot className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
Model Configuration
|
||||
</div>
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ export function LLMConfigForm({
|
|||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
<Sparkles className="h-3.5 w-3.5 text-violet-500" />
|
||||
Configuration Name
|
||||
</FormLabel>
|
||||
|
|
@ -256,7 +256,7 @@ export function LLMConfigForm({
|
|||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-muted-foreground">
|
||||
<FormLabel className="text-muted-foreground text-xs sm:text-sm">
|
||||
Description
|
||||
<Badge variant="outline" className="ml-2 text-[10px]">
|
||||
Optional
|
||||
|
|
@ -277,7 +277,7 @@ export function LLMConfigForm({
|
|||
name="provider"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>LLM Provider</FormLabel>
|
||||
<FormLabel className="text-xs sm:text-sm">LLM Provider</FormLabel>
|
||||
<Select value={field.value} onValueChange={handleProviderChange}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="transition-all focus:ring-violet-500/50">
|
||||
|
|
@ -315,7 +315,7 @@ export function LLMConfigForm({
|
|||
name="custom_provider"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Custom Provider Name</FormLabel>
|
||||
<FormLabel className="text-xs sm:text-sm">Custom Provider Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="my-custom-provider"
|
||||
|
|
@ -337,7 +337,7 @@ export function LLMConfigForm({
|
|||
name="model_name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Model Name</FormLabel>
|
||||
<FormLabel className="text-xs sm:text-sm">Model Name</FormLabel>
|
||||
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
|
|
@ -410,7 +410,7 @@ export function LLMConfigForm({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
{selectedProvider?.example && (
|
||||
<FormDescription className="text-xs">
|
||||
<FormDescription className="text-[10px] sm:text-xs">
|
||||
Example: {selectedProvider.example}
|
||||
</FormDescription>
|
||||
)}
|
||||
|
|
@ -426,7 +426,7 @@ export function LLMConfigForm({
|
|||
name="api_key"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
<Key className="h-3.5 w-3.5 text-amber-500" />
|
||||
API Key
|
||||
</FormLabel>
|
||||
|
|
@ -438,7 +438,7 @@ export function LLMConfigForm({
|
|||
/>
|
||||
</FormControl>
|
||||
{watchProvider === "OLLAMA" && (
|
||||
<FormDescription className="text-xs">
|
||||
<FormDescription className="text-[10px] sm:text-xs">
|
||||
Ollama doesn't require auth — enter any value
|
||||
</FormDescription>
|
||||
)}
|
||||
|
|
@ -452,7 +452,7 @@ export function LLMConfigForm({
|
|||
name="api_base"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-2">
|
||||
<FormLabel className="flex items-center gap-2 text-xs sm:text-sm">
|
||||
API Base URL
|
||||
{selectedProvider?.apiBase && (
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
|
|
@ -510,8 +510,8 @@ export function LLMConfigForm({
|
|||
<>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm font-medium text-muted-foreground">
|
||||
<Sparkles className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
Advanced Parameters
|
||||
</div>
|
||||
|
||||
|
|
@ -542,19 +542,29 @@ export function LLMConfigForm({
|
|||
)}
|
||||
>
|
||||
{onCancel && (
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
className="text-xs sm:text-sm h-9 sm:h-10"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={isSubmitting} className="gap-2 min-w-[160px]">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="gap-2 min-w-[140px] sm:min-w-[160px] text-xs sm:text-sm h-9 sm:h-10"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 animate-spin" />
|
||||
{mode === "edit" ? "Updating..." : "Creating..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!compact && <Rocket className="h-4 w-4" />}
|
||||
{!compact && <Rocket className="h-3.5 w-3.5 sm:h-4 sm:w-4" />}
|
||||
{submitLabel ?? (mode === "edit" ? "Update Configuration" : "Create Configuration")}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,9 +42,15 @@ interface AllChatsSidebarProps {
|
|||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsSidebarProps) {
|
||||
export function AllChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onCloseMobileSidebar,
|
||||
}: AllChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
|
|
@ -61,6 +67,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
const isSearchMode = !!debouncedSearchQuery.trim();
|
||||
|
|
@ -120,8 +127,10 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
(threadId: number) => {
|
||||
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
|
||||
onOpenChange(false);
|
||||
// Also close the main sidebar on mobile
|
||||
onCloseMobileSidebar?.();
|
||||
},
|
||||
[router, onOpenChange, searchSpaceId]
|
||||
[router, onOpenChange, searchSpaceId, onCloseMobileSidebar]
|
||||
);
|
||||
|
||||
// Handle thread deletion
|
||||
|
|
@ -209,7 +218,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-50 bg-black/50"
|
||||
className="fixed inset-0 z-[70] bg-black/50"
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -220,7 +229,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
|
||||
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("all_chats") || "All Chats"}
|
||||
|
|
@ -345,14 +354,17 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
</Tooltip>
|
||||
|
||||
{/* Actions dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
open={openDropdownId === thread.id}
|
||||
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 shrink-0",
|
||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
||||
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isBusy}
|
||||
|
|
@ -365,7 +377,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface AllNotesSidebarProps {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
onAddNote?: () => void;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
export function AllNotesSidebar({
|
||||
|
|
@ -34,6 +35,7 @@ export function AllNotesSidebar({
|
|||
onOpenChange,
|
||||
searchSpaceId,
|
||||
onAddNote,
|
||||
onCloseMobileSidebar,
|
||||
}: AllNotesSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
|
|
@ -45,6 +47,7 @@ export function AllNotesSidebar({
|
|||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
// Handle mounting for portal
|
||||
|
|
@ -114,8 +117,10 @@ export function AllNotesSidebar({
|
|||
(noteId: number, noteSearchSpaceId: number) => {
|
||||
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
|
||||
onOpenChange(false);
|
||||
// Also close the main sidebar on mobile
|
||||
onCloseMobileSidebar?.();
|
||||
},
|
||||
[router, onOpenChange]
|
||||
[router, onOpenChange, onCloseMobileSidebar]
|
||||
);
|
||||
|
||||
// Handle note deletion
|
||||
|
|
@ -195,7 +200,7 @@ export function AllNotesSidebar({
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-50 bg-black/50"
|
||||
className="fixed inset-0 z-[70] bg-black/50"
|
||||
onClick={() => onOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -206,7 +211,7 @@ export function AllNotesSidebar({
|
|||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
|
||||
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("all_notes") || "All Notes"}
|
||||
|
|
@ -307,14 +312,17 @@ export function AllNotesSidebar({
|
|||
</Tooltip>
|
||||
|
||||
{/* Actions dropdown - separate from main click area */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenu
|
||||
open={openDropdownId === note.id}
|
||||
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? note.id : null)}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 shrink-0",
|
||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
||||
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isDeleting}
|
||||
|
|
@ -327,7 +335,7 @@ export function AllNotesSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -73,6 +74,7 @@ export function NavChats({
|
|||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const isMobile = useIsMobile();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
|
||||
|
|
@ -119,7 +121,7 @@ export function NavChats({
|
|||
</CollapsibleTrigger>
|
||||
|
||||
{/* Action buttons - always visible on hover */}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
{searchSpaceId && chats.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -171,7 +173,7 @@ export function NavChats({
|
|||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
"opacity-0 group-hover/chat:opacity-100 focus:opacity-100",
|
||||
"md:opacity-0 md:group-hover/chat:opacity-100 md:focus:opacity-100",
|
||||
"data-[state=open]:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
|
|
@ -242,6 +244,7 @@ export function NavChats({
|
|||
open={isAllChatsSidebarOpen}
|
||||
onOpenChange={setIsAllChatsSidebarOpen}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onCloseMobileSidebar={() => setOpenMobile(false)}
|
||||
/>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useLogsSummary } from "@/hooks/use-logs";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
|
@ -75,6 +76,7 @@ export function NavNotes({
|
|||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const isMobile = useIsMobile();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false);
|
||||
|
|
@ -138,7 +140,7 @@ export function NavNotes({
|
|||
</CollapsibleTrigger>
|
||||
|
||||
{/* Action buttons - always visible on hover */}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
<div className="flex items-center gap-0.5 md:opacity-0 md:group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
{searchSpaceId && notes.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -209,7 +211,7 @@ export function NavNotes({
|
|||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
"opacity-0 group-hover/note:opacity-100 focus:opacity-100",
|
||||
"md:opacity-0 md:group-hover/note:opacity-100 md:focus:opacity-100",
|
||||
"data-[state=open]:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
|
|
@ -293,6 +295,7 @@ export function NavNotes({
|
|||
onOpenChange={setIsAllNotesSidebarOpen}
|
||||
searchSpaceId={searchSpaceId}
|
||||
onAddNote={onAddNote}
|
||||
onCloseMobileSidebar={() => setOpenMobile(false)}
|
||||
/>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function ConnectorsTab({ searchSpaceId }: ConnectorsTabProps) {
|
|||
className="w-full"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-4 p-4">
|
||||
<h3 className="text-xl font-semibold">{t(category.title)}</h3>
|
||||
<h3 className="text-lg sm:text-xl font-semibold">{t(category.title)}</h3>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted">
|
||||
<motion.div
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
>
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>{t("file_size_limit")}</AlertDescription>
|
||||
<AlertDescription className="text-xs sm:text-sm">{t("file_size_limit")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Card className="relative overflow-hidden">
|
||||
|
|
@ -249,7 +249,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
>
|
||||
<Upload className="h-12 w-12 text-muted-foreground" />
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium">{t("drag_drop")}</p>
|
||||
<p className="text-base sm:text-lg font-medium">{t("drag_drop")}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
@ -284,8 +284,10 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{t("selected_files", { count: files.length })}</CardTitle>
|
||||
<CardDescription>
|
||||
<CardTitle className="text-lg sm:text-2xl">
|
||||
{t("selected_files", { count: files.length })}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs sm:text-sm">
|
||||
{t("total_size")}: {formatFileSize(getTotalFileSize())}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
|
@ -313,7 +315,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{file.name}</p>
|
||||
<p className="text-sm sm:text-base font-medium truncate">{file.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatFileSize(file.size)}
|
||||
|
|
@ -361,7 +363,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Button
|
||||
className="w-full py-6 text-base font-medium"
|
||||
className="w-full py-4 sm:py-6 text-sm sm:text-base font-medium"
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading || files.length === 0}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -125,17 +125,19 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
|
|||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CardTitle className="text-lg sm:text-2xl flex items-center gap-2">
|
||||
<IconBrandYoutube className="h-5 w-5" />
|
||||
{t("title")}
|
||||
</CardTitle>
|
||||
<CardDescription>{t("subtitle")}</CardDescription>
|
||||
<CardDescription className="text-xs sm:text-sm">{t("subtitle")}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="video-input">{t("label")}</Label>
|
||||
<Label htmlFor="video-input" className="text-sm sm:text-base">
|
||||
{t("label")}
|
||||
</Label>
|
||||
<TagInput
|
||||
id="video-input"
|
||||
tags={videoTags}
|
||||
|
|
@ -212,14 +214,17 @@ export function YouTubeTab({ searchSpaceId }: YouTubeTabProps) {
|
|||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/documents`)}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || videoTags.length === 0}
|
||||
className="relative overflow-hidden"
|
||||
size="sm"
|
||||
className="relative overflow-hidden text-xs sm:text-sm"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,88 @@ import { z } from "zod";
|
|||
export const TodoStatusSchema = z.enum(["pending", "in_progress", "completed", "cancelled"]);
|
||||
export type TodoStatus = z.infer<typeof TodoStatusSchema>;
|
||||
|
||||
/**
|
||||
* Normalize various status string formats to the canonical TodoStatus
|
||||
* Handles common variations from different sources:
|
||||
* - Linear: Done, In Progress, Todo, Backlog, Cancelled
|
||||
* - Jira: To Do, In Progress, Done, In Review, Reopened, Testing + statusCategory
|
||||
* - ClickUp: Open, In Progress, Complete, Closed, Review
|
||||
* - GitHub: open, closed
|
||||
* - Airtable: Any custom field values
|
||||
*/
|
||||
export function normalizeStatus(status: unknown): TodoStatus {
|
||||
if (typeof status !== "string") return "pending";
|
||||
|
||||
const normalized = status
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[\s_-]+/g, "_");
|
||||
|
||||
// Completed variations
|
||||
// Sources: Linear (Done), Jira (Done), ClickUp (Complete, Closed), GitHub (closed)
|
||||
if (
|
||||
normalized === "completed" ||
|
||||
normalized === "complete" ||
|
||||
normalized === "done" ||
|
||||
normalized === "finished" ||
|
||||
normalized === "closed" ||
|
||||
normalized === "resolved" ||
|
||||
normalized === "fixed" ||
|
||||
normalized === "shipped" ||
|
||||
normalized === "released" ||
|
||||
normalized === "merged"
|
||||
) {
|
||||
return "completed";
|
||||
}
|
||||
|
||||
// In progress variations
|
||||
// Sources: Linear (In Progress), Jira (In Progress, In Review, Testing), ClickUp (In Progress, Review)
|
||||
if (
|
||||
normalized === "in_progress" ||
|
||||
normalized === "inprogress" ||
|
||||
normalized === "started" ||
|
||||
normalized === "active" ||
|
||||
normalized === "working" ||
|
||||
normalized === "in_review" ||
|
||||
normalized === "inreview" ||
|
||||
normalized === "review" ||
|
||||
normalized === "reviewing" ||
|
||||
normalized === "testing" ||
|
||||
normalized === "in_testing" ||
|
||||
normalized === "qa" ||
|
||||
normalized === "in_qa" ||
|
||||
normalized === "doing" ||
|
||||
normalized === "wip" ||
|
||||
normalized === "work_in_progress"
|
||||
) {
|
||||
return "in_progress";
|
||||
}
|
||||
|
||||
// Cancelled variations
|
||||
// Sources: Linear (Cancelled), Jira (Won't Fix, Duplicate)
|
||||
if (
|
||||
normalized === "cancelled" ||
|
||||
normalized === "canceled" ||
|
||||
normalized === "dropped" ||
|
||||
normalized === "won't_fix" ||
|
||||
normalized === "wontfix" ||
|
||||
normalized === "wont_fix" ||
|
||||
normalized === "duplicate" ||
|
||||
normalized === "invalid" ||
|
||||
normalized === "rejected" ||
|
||||
normalized === "archived" ||
|
||||
normalized === "removed" ||
|
||||
normalized === "obsolete"
|
||||
) {
|
||||
return "cancelled";
|
||||
}
|
||||
|
||||
// Pending variations (default)
|
||||
// Sources: Linear (Todo, Backlog), Jira (To Do, Reopened), ClickUp (Open), GitHub (open)
|
||||
// Includes: "pending", "todo", "to_do", "backlog", "open", "new", "triage", "reopened", etc.
|
||||
return "pending";
|
||||
}
|
||||
|
||||
/**
|
||||
* Single todo item in a plan
|
||||
* Matches deepagents TodoListMiddleware output: { content, status }
|
||||
|
|
@ -67,9 +149,7 @@ export function parseSerializablePlan(data: unknown): NormalizedPlan {
|
|||
return {
|
||||
id: typeof todo?.id === "string" ? todo.id : `todo-${i}`,
|
||||
content: typeof todo?.content === "string" ? todo.content : "Task",
|
||||
status: TodoStatusSchema.safeParse(todo?.status).success
|
||||
? (todo.status as TodoStatus)
|
||||
: ("pending" as const),
|
||||
status: normalizeStatus(todo?.status),
|
||||
};
|
||||
})
|
||||
: [{ id: "1", content: "No tasks", status: "pending" as const }],
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue