add jira connector implementation in the web

This commit is contained in:
CREDO23 2025-07-24 11:58:41 +02:00
parent cd05a06a91
commit 6bced733b2
9 changed files with 2740 additions and 2133 deletions

View file

@ -0,0 +1 @@
{"2d0ec64d93969318101ee479b664221b32241665":{"files":{"surfsense_web/lib/connectors/utils.ts":["RXwmTdu3JAyxa1ApFuYJiSRHfZo=",true],"surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx":["jZynb8hLm5uq1viyFK9UMcRClD8=",true],"surfsense_web/app/dashboard/[search_space_id]/researcher/[chat_id]/page.tsx":["LEFIcQIvBUtbTE9PuuJI0WqzdVw=",true]},"modified":1753351069225}}

View file

@ -17,7 +17,6 @@ You are SurfSense, an advanced AI research assistant that provides detailed, wel
- LINEAR_CONNECTOR: "Linear project issues and discussions" (personal project management) - LINEAR_CONNECTOR: "Linear project issues and discussions" (personal project management)
- JIRA_CONNECTOR: "Jira project issues, tickets, and comments" (personal project tracking) - JIRA_CONNECTOR: "Jira project issues, tickets, and comments" (personal project tracking)
- DISCORD_CONNECTOR: "Discord server conversations and shared content" (personal community communications) - DISCORD_CONNECTOR: "Discord server conversations and shared content" (personal community communications)
- DISCORD_CONNECTOR: "Discord server messages and channels" (personal community interactions)
- TAVILY_API: "Tavily search API results" (personalized search results) - TAVILY_API: "Tavily search API results" (personalized search results)
- LINKUP_API: "Linkup search API results" (personalized search results) - LINKUP_API: "Linkup search API results" (personalized search results)
</knowledge_sources> </knowledge_sources>

View file

@ -181,6 +181,26 @@ export default function EditConnectorPage() {
/> />
)} )}
{/* == Jira == */}
{connector.connector_type === "JIRA_CONNECTOR" && (
<div className="space-y-4">
<EditSimpleTokenForm
control={editForm.control}
fieldName="JIRA_BASE_URL"
fieldLabel="Jira Base URL"
fieldDescription="Update your Jira instance URL if needed."
placeholder="https://yourcompany.atlassian.net"
/>
<EditSimpleTokenForm
control={editForm.control}
fieldName="JIRA_PERSONAL_ACCESS_TOKEN"
fieldLabel="Jira Personal Access Token"
fieldDescription="Update your Jira Personal Access Token if needed."
placeholder="Your Jira Personal Access Token"
/>
</div>
)}
{/* == Linkup == */} {/* == Linkup == */}
{connector.connector_type === "LINKUP_API" && ( {connector.connector_type === "LINKUP_API" && (
<EditSimpleTokenForm <EditSimpleTokenForm
@ -202,7 +222,6 @@ export default function EditConnectorPage() {
placeholder="Bot token..." placeholder="Bot token..."
/> />
)} )}
</CardContent> </CardContent>
<CardFooter className="border-t pt-6"> <CardFooter className="border-t pt-6">
<Button <Button

View file

@ -9,7 +9,10 @@ import * as z from "zod";
import { toast } from "sonner"; import { toast } from "sonner";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
import { useSearchSourceConnectors, SearchSourceConnector } from "@/hooks/useSearchSourceConnectors"; import {
useSearchSourceConnectors,
SearchSourceConnector,
} from "@/hooks/useSearchSourceConnectors";
import { import {
Form, Form,
FormControl, FormControl,
@ -28,11 +31,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
// Define the form schema with Zod // Define the form schema with Zod
const apiConnectorFormSchema = z.object({ const apiConnectorFormSchema = z.object({
@ -47,13 +46,15 @@ const apiConnectorFormSchema = z.object({
// Helper function to get connector type display name // Helper function to get connector type display name
const getConnectorTypeDisplay = (type: string): string => { const getConnectorTypeDisplay = (type: string): string => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
"SERPER_API": "Serper API", SERPER_API: "Serper API",
"TAVILY_API": "Tavily API", TAVILY_API: "Tavily API",
"SLACK_CONNECTOR": "Slack Connector", SLACK_CONNECTOR: "Slack Connector",
"NOTION_CONNECTOR": "Notion Connector", NOTION_CONNECTOR: "Notion Connector",
"GITHUB_CONNECTOR": "GitHub Connector", GITHUB_CONNECTOR: "GitHub Connector",
"DISCORD_CONNECTOR": "Discord Connector", LINEAR_CONNECTOR: "Linear Connector",
"LINKUP_API": "Linkup", JIRA_CONNECTOR: "Jira Connector",
DISCORD_CONNECTOR: "Discord Connector",
LINKUP_API: "Linkup",
// Add other connector types here as needed // Add other connector types here as needed
}; };
return typeMap[type] || type; return typeMap[type] || type;
@ -69,7 +70,9 @@ export default function EditConnectorPage() {
const connectorId = parseInt(params.connector_id as string, 10); const connectorId = parseInt(params.connector_id as string, 10);
const { connectors, updateConnector } = useSearchSourceConnectors(); const { connectors, updateConnector } = useSearchSourceConnectors();
const [connector, setConnector] = useState<SearchSourceConnector | null>(null); const [connector, setConnector] = useState<SearchSourceConnector | null>(
null,
);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// console.log("connector", connector); // console.log("connector", connector);
@ -85,20 +88,20 @@ export default function EditConnectorPage() {
// Get API key field name based on connector type // Get API key field name based on connector type
const getApiKeyFieldName = (connectorType: string): string => { const getApiKeyFieldName = (connectorType: string): string => {
const fieldMap: Record<string, string> = { const fieldMap: Record<string, string> = {
"SERPER_API": "SERPER_API_KEY", SERPER_API: "SERPER_API_KEY",
"TAVILY_API": "TAVILY_API_KEY", TAVILY_API: "TAVILY_API_KEY",
"SLACK_CONNECTOR": "SLACK_BOT_TOKEN", SLACK_CONNECTOR: "SLACK_BOT_TOKEN",
"NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN", NOTION_CONNECTOR: "NOTION_INTEGRATION_TOKEN",
"GITHUB_CONNECTOR": "GITHUB_PAT", GITHUB_CONNECTOR: "GITHUB_PAT",
"DISCORD_CONNECTOR": "DISCORD_BOT_TOKEN", DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN",
"LINKUP_API": "LINKUP_API_KEY" LINKUP_API: "LINKUP_API_KEY",
}; };
return fieldMap[connectorType] || ""; return fieldMap[connectorType] || "";
}; };
// Find connector in the list // Find connector in the list
useEffect(() => { useEffect(() => {
const currentConnector = connectors.find(c => c.id === connectorId); const currentConnector = connectors.find((c) => c.id === connectorId);
if (currentConnector) { if (currentConnector) {
setConnector(currentConnector); setConnector(currentConnector);
@ -150,7 +153,9 @@ export default function EditConnectorPage() {
router.push(`/dashboard/${searchSpaceId}/connectors`); router.push(`/dashboard/${searchSpaceId}/connectors`);
} catch (error) { } catch (error) {
console.error("Error updating connector:", error); console.error("Error updating connector:", error);
toast.error(error instanceof Error ? error.message : "Failed to update connector"); toast.error(
error instanceof Error ? error.message : "Failed to update connector",
);
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -186,24 +191,30 @@ export default function EditConnectorPage() {
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardHeader> <CardHeader>
<CardTitle className="text-2xl font-bold"> <CardTitle className="text-2xl font-bold">
Edit {connector ? getConnectorTypeDisplay(connector.connector_type) : ""} Connector Edit{" "}
{connector
? getConnectorTypeDisplay(connector.connector_type)
: ""}{" "}
Connector
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>Update your connector settings.</CardDescription>
Update your connector settings.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Alert className="mb-6 bg-muted"> <Alert className="mb-6 bg-muted">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<AlertTitle>API Key Security</AlertTitle> <AlertTitle>API Key Security</AlertTitle>
<AlertDescription> <AlertDescription>
Your API key is stored securely. For security reasons, we don't display your existing API key. Your API key is stored securely. For security reasons, we don't
If you don't update the API key field, your existing key will be preserved. display your existing API key. If you don't update the API key
field, your existing key will be preserved.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -245,7 +256,8 @@ export default function EditConnectorPage() {
? "Enter new Slack Bot Token (optional)" ? "Enter new Slack Bot Token (optional)"
: connector?.connector_type === "NOTION_CONNECTOR" : connector?.connector_type === "NOTION_CONNECTOR"
? "Enter new Notion Token (optional)" ? "Enter new Notion Token (optional)"
: connector?.connector_type === "GITHUB_CONNECTOR" : connector?.connector_type ===
"GITHUB_CONNECTOR"
? "Enter new GitHub PAT (optional)" ? "Enter new GitHub PAT (optional)"
: connector?.connector_type === "LINKUP_API" : connector?.connector_type === "LINKUP_API"
? "Enter new Linkup API Key (optional)" ? "Enter new Linkup API Key (optional)"

View file

@ -0,0 +1,448 @@
"use client";
import { useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { motion } from "framer-motion";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { toast } from "sonner";
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
// Define the form schema with Zod
const jiraConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
base_url: z
.string()
.url({
message:
"Please enter a valid Jira URL (e.g., https://yourcompany.atlassian.net)",
})
.refine(
(url) => {
return url.includes("atlassian.net") || url.includes("jira");
},
{
message: "Please enter a valid Jira instance URL",
},
),
personal_access_token: z.string().min(10, {
message: "Jira Personal Access Token is required and must be valid.",
}),
});
// Define the type for the form values
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
export default function JiraConnectorPage() {
const router = useRouter();
const params = useParams();
const searchSpaceId = params.search_space_id as string;
const [isSubmitting, setIsSubmitting] = useState(false);
const { createConnector } = useSearchSourceConnectors();
// Initialize the form
const form = useForm<JiraConnectorFormValues>({
resolver: zodResolver(jiraConnectorFormSchema),
defaultValues: {
name: "Jira Connector",
base_url: "",
personal_access_token: "",
},
});
// Handle form submission
const onSubmit = async (values: JiraConnectorFormValues) => {
setIsSubmitting(true);
try {
await createConnector({
name: values.name,
connector_type: "JIRA_CONNECTOR",
config: {
JIRA_BASE_URL: values.base_url,
JIRA_PERSONAL_ACCESS_TOKEN: values.personal_access_token,
},
is_indexable: true,
last_indexed_at: null,
});
toast.success("Jira connector created successfully!");
// Navigate back to connectors page
router.push(`/dashboard/${searchSpaceId}/connectors`);
} catch (error) {
console.error("Error creating connector:", error);
toast.error(
error instanceof Error ? error.message : "Failed to create connector",
);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="container mx-auto py-8 max-w-3xl">
<Button
variant="ghost"
className="mb-6"
onClick={() =>
router.push(`/dashboard/${searchSpaceId}/connectors/add`)
}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Connectors
</Button>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Tabs defaultValue="connect" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="connect">Connect</TabsTrigger>
<TabsTrigger value="documentation">Documentation</TabsTrigger>
</TabsList>
<TabsContent value="connect">
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="text-2xl font-bold">
Connect Jira Instance
</CardTitle>
<CardDescription>
Integrate with Jira to search and retrieve information from
your issues, tickets, and comments. This connector can index
your Jira content for search.
</CardDescription>
</CardHeader>
<CardContent>
<Alert className="mb-6 bg-muted">
<Info className="h-4 w-4" />
<AlertTitle>Jira Personal Access Token Required</AlertTitle>
<AlertDescription>
You'll need a Jira Personal Access Token to use this
connector. You can create one from{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Atlassian Account Settings
</a>
</AlertDescription>
</Alert>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Connector Name</FormLabel>
<FormControl>
<Input placeholder="My Jira Connector" {...field} />
</FormControl>
<FormDescription>
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel>Jira Instance URL</FormLabel>
<FormControl>
<Input
placeholder="https://yourcompany.atlassian.net"
{...field}
/>
</FormControl>
<FormDescription>
Your Jira instance URL. For Atlassian Cloud, this is
typically https://yourcompany.atlassian.net
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="personal_access_token"
render={({ field }) => (
<FormItem>
<FormLabel>Personal Access Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Jira Personal Access Token"
{...field}
/>
</FormControl>
<FormDescription>
Your Jira Personal Access Token will be encrypted
and stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
disabled={isSubmitting}
className="w-full sm:w-auto"
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connecting...
</>
) : (
<>
<Check className="mr-2 h-4 w-4" />
Connect Jira
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
<CardFooter className="flex flex-col items-start border-t bg-muted/50 px-6 py-4">
<h4 className="text-sm font-medium">
What you get with Jira integration:
</h4>
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
<li>Search through all your Jira issues and tickets</li>
<li>
Access issue descriptions, comments, and full discussion
threads
</li>
<li>
Connect your team's project management directly to your
search space
</li>
<li>
Keep your search results up-to-date with latest Jira content
</li>
<li>
Index your Jira issues for enhanced search capabilities
</li>
<li>
Search by issue keys, status, priority, and assignee
information
</li>
</ul>
</CardFooter>
</Card>
</TabsContent>
<TabsContent value="documentation">
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="text-2xl font-bold">
Jira Connector Documentation
</CardTitle>
<CardDescription>
Learn how to set up and use the Jira connector to index your
project management data.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h3 className="text-xl font-semibold mb-2">How it works</h3>
<p className="text-muted-foreground">
The Jira connector uses the Jira REST API to fetch all
issues and comments that the Personal Access Token has
access to within your Jira instance.
</p>
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
<li>
For follow up indexing runs, the connector retrieves
issues and comments that have been updated since the last
indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates
should appear in your search results within minutes.
</li>
</ul>
</div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="authorization">
<AccordionTrigger className="text-lg font-medium">
Authorization
</AccordionTrigger>
<AccordionContent className="space-y-4">
<Alert className="bg-muted">
<Info className="h-4 w-4" />
<AlertTitle>Read-Only Access is Sufficient</AlertTitle>
<AlertDescription>
You only need read access for this connector to work.
The Personal Access Token will only be used to read
your Jira data.
</AlertDescription>
</Alert>
<div className="space-y-6">
<div>
<h4 className="font-medium mb-2">
Step 1: Create a Personal Access Token
</h4>
<ol className="list-decimal pl-5 space-y-3">
<li>Log in to your Atlassian account</li>
<li>
Navigate to{" "}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://id.atlassian.com/manage-profile/security/api-tokens
</a>
</li>
<li>
Click <strong>Create API token</strong>
</li>
<li>
Enter a label for your token (like "SurfSense
Connector")
</li>
<li>
Click <strong>Create</strong>
</li>
<li>
Copy the generated token as it will only be shown
once
</li>
</ol>
</div>
<div>
<h4 className="font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-muted-foreground mb-3">
The Personal Access Token will have access to all
projects and issues that your user account can see.
Make sure your account has appropriate permissions
for the projects you want to index.
</p>
<Alert className="bg-muted">
<Info className="h-4 w-4" />
<AlertTitle>Data Privacy</AlertTitle>
<AlertDescription>
Only issues, comments, and basic metadata will be
indexed. Jira attachments and linked files are not
indexed by this connector.
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="indexing">
<AccordionTrigger className="text-lg font-medium">
Indexing
</AccordionTrigger>
<AccordionContent className="space-y-4">
<ol className="list-decimal pl-5 space-y-3">
<li>
Navigate to the Connector Dashboard and select the{" "}
<strong>Jira</strong> Connector.
</li>
<li>
Enter your <strong>Jira Instance URL</strong> (e.g.,
https://yourcompany.atlassian.net)
</li>
<li>
Place your <strong>Personal Access Token</strong> in
the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the
connection.
</li>
<li>
Once connected, your Jira issues will be indexed
automatically.
</li>
</ol>
<Alert className="bg-muted">
<Info className="h-4 w-4" />
<AlertTitle>What Gets Indexed</AlertTitle>
<AlertDescription>
<p className="mb-2">
The Jira connector indexes the following data:
</p>
<ul className="list-disc pl-5">
<li>Issue keys and summaries (e.g., PROJ-123)</li>
<li>Issue descriptions</li>
<li>Issue comments and discussion threads</li>
<li>
Issue status, priority, and type information
</li>
<li>Assignee and reporter information</li>
<li>Project information</li>
</ul>
</AlertDescription>
</Alert>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</motion.div>
</div>
);
}

View file

@ -1,8 +1,17 @@
"use client"; "use client";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import {
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { import {
IconBrandDiscord, IconBrandDiscord,
IconBrandGithub, IconBrandGithub,
@ -67,23 +76,26 @@ const connectorCategories: ConnectorCategory[] = [
{ {
id: "slack-connector", id: "slack-connector",
title: "Slack", title: "Slack",
description: "Connect to your Slack workspace to access messages and channels.", description:
"Connect to your Slack workspace to access messages and channels.",
icon: <IconBrandSlack className="h-6 w-6" />, icon: <IconBrandSlack className="h-6 w-6" />,
status: "available", status: "available",
}, },
{ {
id: "ms-teams", id: "ms-teams",
title: "Microsoft Teams", title: "Microsoft Teams",
description: "Connect to Microsoft Teams to access your team's conversations.", description:
"Connect to Microsoft Teams to access your team's conversations.",
icon: <IconBrandWindows className="h-6 w-6" />, icon: <IconBrandWindows className="h-6 w-6" />,
status: "coming-soon", status: "coming-soon",
}, },
{ {
id: "discord-connector", id: "discord-connector",
title: "Discord", title: "Discord",
description: "Connect to Discord servers to access messages and channels.", description:
"Connect to Discord servers to access messages and channels.",
icon: <IconBrandDiscord className="h-6 w-6" />, icon: <IconBrandDiscord className="h-6 w-6" />,
status: "available" status: "available",
}, },
], ],
}, },
@ -94,16 +106,18 @@ const connectorCategories: ConnectorCategory[] = [
{ {
id: "linear-connector", id: "linear-connector",
title: "Linear", title: "Linear",
description: "Connect to Linear to search issues, comments and project data.", description:
"Connect to Linear to search issues, comments and project data.",
icon: <IconLayoutKanban className="h-6 w-6" />, icon: <IconLayoutKanban className="h-6 w-6" />,
status: "available", status: "available",
}, },
{ {
id: "jira-connector", id: "jira-connector",
title: "Jira", title: "Jira",
description: "Connect to Jira to search issues, tickets and project data.", description:
"Connect to Jira to search issues, tickets and project data.",
icon: <IconTicket className="h-6 w-6" />, icon: <IconTicket className="h-6 w-6" />,
status: "coming-soon", status: "available",
}, },
], ],
}, },
@ -114,14 +128,16 @@ const connectorCategories: ConnectorCategory[] = [
{ {
id: "notion-connector", id: "notion-connector",
title: "Notion", title: "Notion",
description: "Connect to your Notion workspace to access pages and databases.", description:
"Connect to your Notion workspace to access pages and databases.",
icon: <IconBrandNotion className="h-6 w-6" />, icon: <IconBrandNotion className="h-6 w-6" />,
status: "available", status: "available",
}, },
{ {
id: "github-connector", id: "github-connector",
title: "GitHub", title: "GitHub",
description: "Connect a GitHub PAT to index code and docs from accessible repositories.", description:
"Connect a GitHub PAT to index code and docs from accessible repositories.",
icon: <IconBrandGithub className="h-6 w-6" />, icon: <IconBrandGithub className="h-6 w-6" />,
status: "available", status: "available",
}, },
@ -141,7 +157,8 @@ const connectorCategories: ConnectorCategory[] = [
{ {
id: "zoom", id: "zoom",
title: "Zoom", title: "Zoom",
description: "Connect to Zoom to access meeting recordings and transcripts.", description:
"Connect to Zoom to access meeting recordings and transcripts.",
icon: <IconBrandZoom className="h-6 w-6" />, icon: <IconBrandZoom className="h-6 w-6" />,
status: "coming-soon", status: "coming-soon",
}, },
@ -152,7 +169,7 @@ const connectorCategories: ConnectorCategory[] = [
// Animation variants // Animation variants
const fadeIn = { const fadeIn = {
hidden: { opacity: 0 }, hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.4 } } visible: { opacity: 1, transition: { duration: 0.4 } },
}; };
const staggerContainer = { const staggerContainer = {
@ -160,9 +177,9 @@ const staggerContainer = {
visible: { visible: {
opacity: 1, opacity: 1,
transition: { transition: {
staggerChildren: 0.1 staggerChildren: 0.1,
} },
} },
}; };
const cardVariants = { const cardVariants = {
@ -173,30 +190,36 @@ const cardVariants = {
transition: { transition: {
type: "spring", type: "spring",
stiffness: 260, stiffness: 260,
damping: 20 damping: 20,
} },
}, },
hover: { hover: {
scale: 1.02, scale: 1.02,
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", boxShadow:
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
transition: { transition: {
type: "spring", type: "spring",
stiffness: 400, stiffness: 400,
damping: 10 damping: 10,
} },
} },
}; };
export default function ConnectorsPage() { export default function ConnectorsPage() {
const params = useParams(); const params = useParams();
const searchSpaceId = params.search_space_id as string; const searchSpaceId = params.search_space_id as string;
const [expandedCategories, setExpandedCategories] = useState<string[]>(["search-engines", "knowledge-bases", "project-management", "team-chats"]); const [expandedCategories, setExpandedCategories] = useState<string[]>([
"search-engines",
"knowledge-bases",
"project-management",
"team-chats",
]);
const toggleCategory = (categoryId: string) => { const toggleCategory = (categoryId: string) => {
setExpandedCategories(prev => setExpandedCategories((prev) =>
prev.includes(categoryId) prev.includes(categoryId)
? prev.filter(id => id !== categoryId) ? prev.filter((id) => id !== categoryId)
: [...prev, categoryId] : [...prev, categoryId],
); );
}; };
@ -207,7 +230,7 @@ export default function ConnectorsPage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
duration: 0.6, duration: 0.6,
ease: [0.22, 1, 0.36, 1] ease: [0.22, 1, 0.36, 1],
}} }}
className="mb-12 text-center" className="mb-12 text-center"
> >
@ -215,7 +238,8 @@ export default function ConnectorsPage() {
Connect Your Tools Connect Your Tools
</h1> </h1>
<p className="text-muted-foreground mt-3 text-lg max-w-2xl mx-auto"> <p className="text-muted-foreground mt-3 text-lg max-w-2xl mx-auto">
Integrate with your favorite services to enhance your research capabilities. Integrate with your favorite services to enhance your research
capabilities.
</p> </p>
</motion.div> </motion.div>
@ -239,9 +263,17 @@ export default function ConnectorsPage() {
<div className="flex items-center justify-between space-x-4 p-4"> <div className="flex items-center justify-between space-x-4 p-4">
<h3 className="text-xl font-semibold">{category.title}</h3> <h3 className="text-xl font-semibold">{category.title}</h3>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0 hover:bg-muted"> <Button
variant="ghost"
size="sm"
className="w-9 p-0 hover:bg-muted"
>
<motion.div <motion.div
animate={{ rotate: expandedCategories.includes(category.id) ? 180 : 0 }} animate={{
rotate: expandedCategories.includes(category.id)
? 180
: 0,
}}
transition={{ duration: 0.3, ease: "easeInOut" }} transition={{ duration: 0.3, ease: "easeInOut" }}
> >
<IconChevronDown className="h-5 w-5" /> <IconChevronDown className="h-5 w-5" />
@ -279,14 +311,22 @@ export default function ConnectorsPage() {
</div> </div>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-medium">{connector.title}</h3> <h3 className="font-medium">
{connector.title}
</h3>
{connector.status === "coming-soon" && ( {connector.status === "coming-soon" && (
<Badge variant="outline" className="text-xs bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800"> <Badge
variant="outline"
className="text-xs bg-amber-100 dark:bg-amber-950 text-amber-800 dark:text-amber-300 border-amber-200 dark:border-amber-800"
>
Coming soon Coming soon
</Badge> </Badge>
)} )}
{connector.status === "connected" && ( {connector.status === "connected" && (
<Badge variant="outline" className="text-xs bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800"> <Badge
variant="outline"
className="text-xs bg-green-100 dark:bg-green-950 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800"
>
Connected Connected
</Badge> </Badge>
)} )}
@ -301,28 +341,45 @@ export default function ConnectorsPage() {
</CardContent> </CardContent>
<CardFooter className="mt-auto pt-2"> <CardFooter className="mt-auto pt-2">
{connector.status === 'available' && ( {connector.status === "available" && (
<Link href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`} className="w-full"> <Link
<Button variant="default" className="w-full group"> href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`}
className="w-full"
>
<Button
variant="default"
className="w-full group"
>
<span>Connect</span> <span>Connect</span>
<motion.div <motion.div
className="ml-1" className="ml-1"
initial={{ x: 0 }} initial={{ x: 0 }}
whileHover={{ x: 3 }} whileHover={{ x: 3 }}
transition={{ type: "spring", stiffness: 400, damping: 10 }} transition={{
type: "spring",
stiffness: 400,
damping: 10,
}}
> >
<IconChevronRight className="h-4 w-4" /> <IconChevronRight className="h-4 w-4" />
</motion.div> </motion.div>
</Button> </Button>
</Link> </Link>
)} )}
{connector.status === 'coming-soon' && ( {connector.status === "coming-soon" && (
<Button variant="outline" disabled className="w-full opacity-70"> <Button
variant="outline"
disabled
className="w-full opacity-70"
>
Coming Soon Coming Soon
</Button> </Button>
)} )}
{connector.status === 'connected' && ( {connector.status === "connected" && (
<Button variant="outline" className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950"> <Button
variant="outline"
className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950"
>
Manage Manage
</Button> </Button>
)} )}

View file

@ -103,6 +103,7 @@ type DocumentType =
| "YOUTUBE_VIDEO" | "YOUTUBE_VIDEO"
| "GITHUB_CONNECTOR" | "GITHUB_CONNECTOR"
| "LINEAR_CONNECTOR" | "LINEAR_CONNECTOR"
| "JIRA_CONNECTOR"
| "DISCORD_CONNECTOR"; | "DISCORD_CONNECTOR";
/** /**
@ -982,12 +983,14 @@ const ChatPage = () => {
if (!message.annotations) return null; if (!message.annotations) return null;
// Get all TERMINAL_INFO annotations content // Get all TERMINAL_INFO annotations content
const terminalInfoAnnotations = (message.annotations as any[]).map(item => { const terminalInfoAnnotations = (message.annotations as any[])
.map((item) => {
if (item.type === "TERMINAL_INFO") { if (item.type === "TERMINAL_INFO") {
return item.content.map((a: any) => a.text) return item.content.map((a: any) => a.text);
} }
}).flat().filter(Boolean) })
.flat()
.filter(Boolean);
// Render the content of the latest TERMINAL_INFO annotation // Render the content of the latest TERMINAL_INFO annotation
return terminalInfoAnnotations.map((item: any, idx: number) => ( return terminalInfoAnnotations.map((item: any, idx: number) => (
@ -1328,29 +1331,41 @@ const ChatPage = () => {
} }
// Fallback to the message content if no ANSWER annotation is available // Fallback to the message content if no ANSWER annotation is available
return <MarkdownViewer return (
<MarkdownViewer
content={message.content} content={message.content}
getCitationSource={(id) => getCitationSource(id, index)} getCitationSource={(id) =>
getCitationSource(id, index)
}
type="ai" type="ai"
/>; />
);
})()} })()}
</div> </div>
} }
</div> </div>
{/* Further Questions Section */} {/* Further Questions Section */}
{message.annotations && (() => { {message.annotations &&
(() => {
// Get all FURTHER_QUESTIONS annotations // Get all FURTHER_QUESTIONS annotations
const furtherQuestionsAnnotations = (message.annotations as any[]) const furtherQuestionsAnnotations = (
.filter(a => a.type === 'FURTHER_QUESTIONS'); message.annotations as any[]
).filter((a) => a.type === "FURTHER_QUESTIONS");
// Get the latest FURTHER_QUESTIONS annotation // Get the latest FURTHER_QUESTIONS annotation
const latestFurtherQuestions = furtherQuestionsAnnotations.length > 0 const latestFurtherQuestions =
? furtherQuestionsAnnotations[furtherQuestionsAnnotations.length - 1] furtherQuestionsAnnotations.length > 0
? furtherQuestionsAnnotations[
furtherQuestionsAnnotations.length - 1
]
: null; : null;
// Only render if we have questions // Only render if we have questions
if (!latestFurtherQuestions?.content || latestFurtherQuestions.content.length === 0) { if (
!latestFurtherQuestions?.content ||
latestFurtherQuestions.content.length === 0
) {
return null; return null;
} }
@ -1364,13 +1379,24 @@ const ChatPage = () => {
<div className="bg-muted/50 border-b border-border/40 px-4 py-2.5"> <div className="bg-muted/50 border-b border-border/40 px-4 py-2.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-muted-foreground flex items-center gap-2"> <h3 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg> </svg>
Follow-up Questions Follow-up Questions
</h3> </h3>
<span className="text-xs text-muted-foreground bg-background/60 px-2 py-1 rounded-full border border-border/40"> <span className="text-xs text-muted-foreground bg-background/60 px-2 py-1 rounded-full border border-border/40">
{furtherQuestions.length} suggestion{furtherQuestions.length !== 1 ? 's' : ''} {furtherQuestions.length} suggestion
{furtherQuestions.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
</div> </div>
@ -1387,7 +1413,8 @@ const ChatPage = () => {
{/* Scrollable container */} {/* Scrollable container */}
<div className="overflow-x-auto scrollbar-hover"> <div className="overflow-x-auto scrollbar-hover">
<div className="flex gap-2 py-1 px-6"> <div className="flex gap-2 py-1 px-6">
{furtherQuestions.map((question: any, qIndex: number) => ( {furtherQuestions.map(
(question: any, qIndex: number) => (
<Button <Button
key={question.id || qIndex} key={question.id || qIndex}
variant="outline" variant="outline"
@ -1396,18 +1423,26 @@ const ChatPage = () => {
onClick={() => { onClick={() => {
// Set the input value and submit // Set the input value and submit
handleInputChange({ handleInputChange({
target: { value: question.question } target: {
value: question.question,
},
} as React.ChangeEvent<HTMLInputElement>); } as React.ChangeEvent<HTMLInputElement>);
// Small delay to ensure input is updated, then submit // Small delay to ensure input is updated, then submit
setTimeout(() => { setTimeout(() => {
const form = document.querySelector('form') as HTMLFormElement; const form =
if (form && status === 'ready') { document.querySelector(
"form",
) as HTMLFormElement;
if (
form &&
status === "ready"
) {
form.requestSubmit(); form.requestSubmit();
} }
}, 50); }, 50);
}} }}
disabled={status !== 'ready'} disabled={status !== "ready"}
> >
<span className="text-foreground group-hover:text-primary transition-colors"> <span className="text-foreground group-hover:text-primary transition-colors">
{question.question} {question.question}
@ -1427,7 +1462,8 @@ const ChatPage = () => {
/> />
</svg> </svg>
</Button> </Button>
))} ),
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
import { import {
ChevronDown, ChevronDown,
Plus, Plus,
@ -12,77 +12,98 @@ import {
Webhook, Webhook,
MessageCircle, MessageCircle,
FileText, FileText,
} from 'lucide-react'; } from "lucide-react";
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus, IconBrandDiscord } from "@tabler/icons-react"; import {
import { Button } from '@/components/ui/button'; IconBrandNotion,
import { Connector, ResearchMode } from './types'; IconBrandSlack,
IconBrandYoutube,
IconBrandGithub,
IconLayoutKanban,
IconLinkPlus,
IconBrandDiscord,
IconTicket,
} from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Connector, ResearchMode } from "./types";
// Helper function to get connector icon // Helper function to get connector icon
export const getConnectorIcon = (connectorType: string) => { export const getConnectorIcon = (connectorType: string) => {
const iconProps = { className: "h-4 w-4" }; const iconProps = { className: "h-4 w-4" };
switch (connectorType) { switch (connectorType) {
case 'LINKUP_API': case "LINKUP_API":
return <IconLinkPlus {...iconProps} />; return <IconLinkPlus {...iconProps} />;
case 'LINEAR_CONNECTOR': case "LINEAR_CONNECTOR":
return <IconLayoutKanban {...iconProps} />; return <IconLayoutKanban {...iconProps} />;
case 'GITHUB_CONNECTOR': case "GITHUB_CONNECTOR":
return <IconBrandGithub {...iconProps} />; return <IconBrandGithub {...iconProps} />;
case 'YOUTUBE_VIDEO': case "YOUTUBE_VIDEO":
return <IconBrandYoutube {...iconProps} />; return <IconBrandYoutube {...iconProps} />;
case 'CRAWLED_URL': case "CRAWLED_URL":
return <Globe {...iconProps} />; return <Globe {...iconProps} />;
case 'FILE': case "FILE":
return <File {...iconProps} />; return <File {...iconProps} />;
case 'EXTENSION': case "EXTENSION":
return <Webhook {...iconProps} />; return <Webhook {...iconProps} />;
case 'SERPER_API': case "SERPER_API":
case 'TAVILY_API': case "TAVILY_API":
return <Link {...iconProps} />; return <Link {...iconProps} />;
case 'SLACK_CONNECTOR': case "SLACK_CONNECTOR":
return <IconBrandSlack {...iconProps} />; return <IconBrandSlack {...iconProps} />;
case 'NOTION_CONNECTOR': case "NOTION_CONNECTOR":
return <IconBrandNotion {...iconProps} />; return <IconBrandNotion {...iconProps} />;
case 'DISCORD_CONNECTOR': case "DISCORD_CONNECTOR":
return <IconBrandDiscord {...iconProps} />; return <IconBrandDiscord {...iconProps} />;
case 'DEEP': case "JIRA_CONNECTOR":
return <IconTicket {...iconProps} />;
case "DEEP":
return <Sparkles {...iconProps} />; return <Sparkles {...iconProps} />;
case 'DEEPER': case "DEEPER":
return <Microscope {...iconProps} />; return <Microscope {...iconProps} />;
case 'DEEPEST': case "DEEPEST":
return <Telescope {...iconProps} />; return <Telescope {...iconProps} />;
default: default:
return <Search {...iconProps} />; return <Search {...iconProps} />;
} }
}; };
export const researcherOptions: { value: ResearchMode; label: string; icon: React.ReactNode }[] = [ export const researcherOptions: {
value: ResearchMode;
label: string;
icon: React.ReactNode;
}[] = [
{ {
value: 'QNA', value: "QNA",
label: 'Q/A', label: "Q/A",
icon: getConnectorIcon('GENERAL') icon: getConnectorIcon("GENERAL"),
}, },
{ {
value: 'REPORT_GENERAL', value: "REPORT_GENERAL",
label: 'General', label: "General",
icon: getConnectorIcon('GENERAL') icon: getConnectorIcon("GENERAL"),
}, },
{ {
value: 'REPORT_DEEP', value: "REPORT_DEEP",
label: 'Deep', label: "Deep",
icon: getConnectorIcon('DEEP') icon: getConnectorIcon("DEEP"),
}, },
{ {
value: 'REPORT_DEEPER', value: "REPORT_DEEPER",
label: 'Deeper', label: "Deeper",
icon: getConnectorIcon('DEEPER') icon: getConnectorIcon("DEEPER"),
}, },
] ];
/** /**
* Displays a small icon for a connector type * Displays a small icon for a connector type
*/ */
export const ConnectorIcon = ({ type, index = 0 }: { type: string; index?: number }) => ( export const ConnectorIcon = ({
type,
index = 0,
}: {
type: string;
index?: number;
}) => (
<div <div
className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background" className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background"
style={{ zIndex: 10 - index }} style={{ zIndex: 10 - index }}
@ -109,15 +130,21 @@ type ConnectorButtonProps = {
/** /**
* Button that displays selected connectors and opens connector selection dialog * Button that displays selected connectors and opens connector selection dialog
*/ */
export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources }: ConnectorButtonProps) => { export const ConnectorButton = ({
selectedConnectors,
onClick,
connectorSources,
}: ConnectorButtonProps) => {
const totalConnectors = connectorSources.length; const totalConnectors = connectorSources.length;
const selectedCount = selectedConnectors.length; const selectedCount = selectedConnectors.length;
const progressPercentage = (selectedCount / totalConnectors) * 100; const progressPercentage = (selectedCount / totalConnectors) * 100;
// Get the name of a single selected connector // Get the name of a single selected connector
const getSingleConnectorName = () => { const getSingleConnectorName = () => {
const connector = connectorSources.find(c => c.type === selectedConnectors[0]); const connector = connectorSources.find(
return connector?.name || ''; (c) => c.type === selectedConnectors[0],
);
return connector?.name || "";
}; };
// Get display text based on selection count // Get display text based on selection count
@ -158,14 +185,18 @@ export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources
variant="outline" variant="outline"
className="h-8 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group" className="h-8 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group"
onClick={onClick} onClick={onClick}
aria-label={selectedCount === 0 ? "Select Connectors" : `${selectedCount} connectors selected`} aria-label={
selectedCount === 0
? "Select Connectors"
: `${selectedCount} connectors selected`
}
> >
{/* Progress indicator */} {/* Progress indicator */}
<div <div
className="absolute bottom-0 left-0 h-1 bg-primary" className="absolute bottom-0 left-0 h-1 bg-primary"
style={{ style={{
width: `${progressPercentage}%`, width: `${progressPercentage}%`,
transition: 'width 0.3s ease' transition: "width 0.3s ease",
}} }}
/> />
@ -183,29 +214,32 @@ type ResearchModeControlProps = {
onChange: (value: ResearchMode) => void; onChange: (value: ResearchMode) => void;
}; };
export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProps) => { export const ResearchModeControl = ({
value,
onChange,
}: ResearchModeControlProps) => {
// Determine if we're in Q/A mode or Report mode // Determine if we're in Q/A mode or Report mode
const isQnaMode = value === 'QNA'; const isQnaMode = value === "QNA";
const isReportMode = value.startsWith('REPORT_'); const isReportMode = value.startsWith("REPORT_");
// Get the current report sub-mode // Get the current report sub-mode
const getCurrentReportMode = () => { const getCurrentReportMode = () => {
if (!isReportMode) return 'GENERAL'; if (!isReportMode) return "GENERAL";
return value.replace('REPORT_', '') as 'GENERAL' | 'DEEP' | 'DEEPER'; return value.replace("REPORT_", "") as "GENERAL" | "DEEP" | "DEEPER";
}; };
const reportSubOptions = [ const reportSubOptions = [
{ value: 'GENERAL', label: 'General', icon: getConnectorIcon('GENERAL') }, { value: "GENERAL", label: "General", icon: getConnectorIcon("GENERAL") },
{ value: 'DEEP', label: 'Deep', icon: getConnectorIcon('DEEP') }, { value: "DEEP", label: "Deep", icon: getConnectorIcon("DEEP") },
{ value: 'DEEPER', label: 'Deeper', icon: getConnectorIcon('DEEPER') }, { value: "DEEPER", label: "Deeper", icon: getConnectorIcon("DEEPER") },
]; ];
const handleModeToggle = (mode: 'QNA' | 'REPORT') => { const handleModeToggle = (mode: "QNA" | "REPORT") => {
if (mode === 'QNA') { if (mode === "QNA") {
onChange('QNA'); onChange("QNA");
} else { } else {
// Default to GENERAL for Report mode // Default to GENERAL for Report mode
onChange('REPORT_GENERAL'); onChange("REPORT_GENERAL");
} }
}; };
@ -220,10 +254,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
<button <button
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${ className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
isQnaMode isQnaMode
? 'bg-primary text-primary-foreground' ? "bg-primary text-primary-foreground"
: 'hover:bg-muted text-muted-foreground hover:text-foreground' : "hover:bg-muted text-muted-foreground hover:text-foreground"
}`} }`}
onClick={() => handleModeToggle('QNA')} onClick={() => handleModeToggle("QNA")}
aria-pressed={isQnaMode} aria-pressed={isQnaMode}
> >
<MessageCircle className="h-3 w-3" /> <MessageCircle className="h-3 w-3" />
@ -232,10 +266,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
<button <button
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${ className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
isReportMode isReportMode
? 'bg-primary text-primary-foreground' ? "bg-primary text-primary-foreground"
: 'hover:bg-muted text-muted-foreground hover:text-foreground' : "hover:bg-muted text-muted-foreground hover:text-foreground"
}`} }`}
onClick={() => handleModeToggle('REPORT')} onClick={() => handleModeToggle("REPORT")}
aria-pressed={isReportMode} aria-pressed={isReportMode}
> >
<FileText className="h-3 w-3" /> <FileText className="h-3 w-3" />
@ -251,8 +285,8 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
key={option.value} key={option.value}
className={`flex h-full items-center gap-1 px-2 text-xs font-medium transition-colors whitespace-nowrap ${ className={`flex h-full items-center gap-1 px-2 text-xs font-medium transition-colors whitespace-nowrap ${
getCurrentReportMode() === option.value getCurrentReportMode() === option.value
? 'bg-primary text-primary-foreground' ? "bg-primary text-primary-foreground"
: 'hover:bg-muted text-muted-foreground hover:text-foreground' : "hover:bg-muted text-muted-foreground hover:text-foreground"
}`} }`}
onClick={() => handleReportSubModeChange(option.value)} onClick={() => handleReportSubModeChange(option.value)}
aria-pressed={getCurrentReportMode() === option.value} aria-pressed={getCurrentReportMode() === option.value}

View file

@ -1,14 +1,15 @@
// Helper function to get connector type display name // Helper function to get connector type display name
export const getConnectorTypeDisplay = (type: string): string => { export const getConnectorTypeDisplay = (type: string): string => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
"SERPER_API": "Serper API", SERPER_API: "Serper API",
"TAVILY_API": "Tavily API", TAVILY_API: "Tavily API",
"SLACK_CONNECTOR": "Slack", SLACK_CONNECTOR: "Slack",
"NOTION_CONNECTOR": "Notion", NOTION_CONNECTOR: "Notion",
"GITHUB_CONNECTOR": "GitHub", GITHUB_CONNECTOR: "GitHub",
"LINEAR_CONNECTOR": "Linear", LINEAR_CONNECTOR: "Linear",
"DISCORD_CONNECTOR": "Discord", JIRA_CONNECTOR: "Jira",
"LINKUP_API": "Linkup", DISCORD_CONNECTOR: "Discord",
LINKUP_API: "Linkup",
}; };
return typeMap[type] || type; return typeMap[type] || type;
}; };