mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-01 18:19:08 +00:00
add jira connector implementation in the web
This commit is contained in:
parent
cd05a06a91
commit
6bced733b2
9 changed files with 2740 additions and 2133 deletions
1
node_modules/.cache/prettier/.prettier-caches/a2ecb2962bf19c1099cfe708e42daa0097f94976.json
generated
vendored
Normal file
1
node_modules/.cache/prettier/.prettier-caches/a2ecb2962bf19c1099cfe708e42daa0097f94976.json
generated
vendored
Normal 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}}
|
|
@ -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)
|
||||
- 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 messages and channels" (personal community interactions)
|
||||
- TAVILY_API: "Tavily search API results" (personalized search results)
|
||||
- LINKUP_API: "Linkup search API results" (personalized search results)
|
||||
</knowledge_sources>
|
||||
|
|
|
@ -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 == */}
|
||||
{connector.connector_type === "LINKUP_API" && (
|
||||
<EditSimpleTokenForm
|
||||
|
@ -202,7 +222,6 @@ export default function EditConnectorPage() {
|
|||
placeholder="Bot token..."
|
||||
/>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-6">
|
||||
<Button
|
||||
|
|
|
@ -9,7 +9,10 @@ import * as z from "zod";
|
|||
import { toast } from "sonner";
|
||||
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
||||
|
||||
import { useSearchSourceConnectors, SearchSourceConnector } from "@/hooks/useSearchSourceConnectors";
|
||||
import {
|
||||
useSearchSourceConnectors,
|
||||
SearchSourceConnector,
|
||||
} from "@/hooks/useSearchSourceConnectors";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
@ -28,11 +31,7 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
// Define the form schema with Zod
|
||||
const apiConnectorFormSchema = z.object({
|
||||
|
@ -47,13 +46,15 @@ const apiConnectorFormSchema = z.object({
|
|||
// Helper function to get connector type display name
|
||||
const getConnectorTypeDisplay = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
"SERPER_API": "Serper API",
|
||||
"TAVILY_API": "Tavily API",
|
||||
"SLACK_CONNECTOR": "Slack Connector",
|
||||
"NOTION_CONNECTOR": "Notion Connector",
|
||||
"GITHUB_CONNECTOR": "GitHub Connector",
|
||||
"DISCORD_CONNECTOR": "Discord Connector",
|
||||
"LINKUP_API": "Linkup",
|
||||
SERPER_API: "Serper API",
|
||||
TAVILY_API: "Tavily API",
|
||||
SLACK_CONNECTOR: "Slack Connector",
|
||||
NOTION_CONNECTOR: "Notion Connector",
|
||||
GITHUB_CONNECTOR: "GitHub Connector",
|
||||
LINEAR_CONNECTOR: "Linear Connector",
|
||||
JIRA_CONNECTOR: "Jira Connector",
|
||||
DISCORD_CONNECTOR: "Discord Connector",
|
||||
LINKUP_API: "Linkup",
|
||||
// Add other connector types here as needed
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
|
@ -69,7 +70,9 @@ export default function EditConnectorPage() {
|
|||
const connectorId = parseInt(params.connector_id as string, 10);
|
||||
|
||||
const { connectors, updateConnector } = useSearchSourceConnectors();
|
||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// console.log("connector", connector);
|
||||
|
@ -85,20 +88,20 @@ export default function EditConnectorPage() {
|
|||
// Get API key field name based on connector type
|
||||
const getApiKeyFieldName = (connectorType: string): string => {
|
||||
const fieldMap: Record<string, string> = {
|
||||
"SERPER_API": "SERPER_API_KEY",
|
||||
"TAVILY_API": "TAVILY_API_KEY",
|
||||
"SLACK_CONNECTOR": "SLACK_BOT_TOKEN",
|
||||
"NOTION_CONNECTOR": "NOTION_INTEGRATION_TOKEN",
|
||||
"GITHUB_CONNECTOR": "GITHUB_PAT",
|
||||
"DISCORD_CONNECTOR": "DISCORD_BOT_TOKEN",
|
||||
"LINKUP_API": "LINKUP_API_KEY"
|
||||
SERPER_API: "SERPER_API_KEY",
|
||||
TAVILY_API: "TAVILY_API_KEY",
|
||||
SLACK_CONNECTOR: "SLACK_BOT_TOKEN",
|
||||
NOTION_CONNECTOR: "NOTION_INTEGRATION_TOKEN",
|
||||
GITHUB_CONNECTOR: "GITHUB_PAT",
|
||||
DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN",
|
||||
LINKUP_API: "LINKUP_API_KEY",
|
||||
};
|
||||
return fieldMap[connectorType] || "";
|
||||
};
|
||||
|
||||
// Find connector in the list
|
||||
useEffect(() => {
|
||||
const currentConnector = connectors.find(c => c.id === connectorId);
|
||||
const currentConnector = connectors.find((c) => c.id === connectorId);
|
||||
|
||||
if (currentConnector) {
|
||||
setConnector(currentConnector);
|
||||
|
@ -150,7 +153,9 @@ export default function EditConnectorPage() {
|
|||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||
} catch (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 {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
@ -186,24 +191,30 @@ export default function EditConnectorPage() {
|
|||
<Card className="border-2 border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
Edit {connector ? getConnectorTypeDisplay(connector.connector_type) : ""} Connector
|
||||
Edit{" "}
|
||||
{connector
|
||||
? getConnectorTypeDisplay(connector.connector_type)
|
||||
: ""}{" "}
|
||||
Connector
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Update your connector settings.
|
||||
</CardDescription>
|
||||
<CardDescription>Update your connector settings.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert className="mb-6 bg-muted">
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertTitle>API Key Security</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your API key is stored securely. For security reasons, we don't display your existing API key.
|
||||
If you don't update the API key field, your existing key will be preserved.
|
||||
Your API key is stored securely. For security reasons, we don't
|
||||
display your existing API key. If you don't update the API key
|
||||
field, your existing key will be preserved.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
|
@ -245,7 +256,8 @@ export default function EditConnectorPage() {
|
|||
? "Enter new Slack Bot Token (optional)"
|
||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
||||
? "Enter new Notion Token (optional)"
|
||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
||||
: connector?.connector_type ===
|
||||
"GITHUB_CONNECTOR"
|
||||
? "Enter new GitHub PAT (optional)"
|
||||
: connector?.connector_type === "LINKUP_API"
|
||||
? "Enter new Linkup API Key (optional)"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,17 @@
|
|||
"use client";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
IconBrandDiscord,
|
||||
IconBrandGithub,
|
||||
|
@ -67,23 +76,26 @@ const connectorCategories: ConnectorCategory[] = [
|
|||
{
|
||||
id: "slack-connector",
|
||||
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" />,
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
id: "ms-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" />,
|
||||
status: "coming-soon",
|
||||
},
|
||||
{
|
||||
id: "discord-connector",
|
||||
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" />,
|
||||
status: "available"
|
||||
status: "available",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -94,16 +106,18 @@ const connectorCategories: ConnectorCategory[] = [
|
|||
{
|
||||
id: "linear-connector",
|
||||
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" />,
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
id: "jira-connector",
|
||||
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" />,
|
||||
status: "coming-soon",
|
||||
status: "available",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -114,14 +128,16 @@ const connectorCategories: ConnectorCategory[] = [
|
|||
{
|
||||
id: "notion-connector",
|
||||
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" />,
|
||||
status: "available",
|
||||
},
|
||||
{
|
||||
id: "github-connector",
|
||||
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" />,
|
||||
status: "available",
|
||||
},
|
||||
|
@ -141,7 +157,8 @@ const connectorCategories: ConnectorCategory[] = [
|
|||
{
|
||||
id: "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" />,
|
||||
status: "coming-soon",
|
||||
},
|
||||
|
@ -152,7 +169,7 @@ const connectorCategories: ConnectorCategory[] = [
|
|||
// Animation variants
|
||||
const fadeIn = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { duration: 0.4 } }
|
||||
visible: { opacity: 1, transition: { duration: 0.4 } },
|
||||
};
|
||||
|
||||
const staggerContainer = {
|
||||
|
@ -160,9 +177,9 @@ const staggerContainer = {
|
|||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1
|
||||
}
|
||||
}
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const cardVariants = {
|
||||
|
@ -173,30 +190,36 @@ const cardVariants = {
|
|||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20
|
||||
}
|
||||
damping: 20,
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
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: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 10
|
||||
}
|
||||
}
|
||||
damping: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function ConnectorsPage() {
|
||||
const params = useParams();
|
||||
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) => {
|
||||
setExpandedCategories(prev =>
|
||||
setExpandedCategories((prev) =>
|
||||
prev.includes(categoryId)
|
||||
? prev.filter(id => id !== categoryId)
|
||||
: [...prev, categoryId]
|
||||
? prev.filter((id) => id !== categoryId)
|
||||
: [...prev, categoryId],
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -207,7 +230,7 @@ export default function ConnectorsPage() {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
ease: [0.22, 1, 0.36, 1]
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
}}
|
||||
className="mb-12 text-center"
|
||||
>
|
||||
|
@ -215,7 +238,8 @@ export default function ConnectorsPage() {
|
|||
Connect Your Tools
|
||||
</h1>
|
||||
<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>
|
||||
</motion.div>
|
||||
|
||||
|
@ -239,9 +263,17 @@ export default function ConnectorsPage() {
|
|||
<div className="flex items-center justify-between space-x-4 p-4">
|
||||
<h3 className="text-xl font-semibold">{category.title}</h3>
|
||||
<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
|
||||
animate={{ rotate: expandedCategories.includes(category.id) ? 180 : 0 }}
|
||||
animate={{
|
||||
rotate: expandedCategories.includes(category.id)
|
||||
? 180
|
||||
: 0,
|
||||
}}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
<IconChevronDown className="h-5 w-5" />
|
||||
|
@ -279,14 +311,22 @@ export default function ConnectorsPage() {
|
|||
</div>
|
||||
<div>
|
||||
<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" && (
|
||||
<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
|
||||
</Badge>
|
||||
)}
|
||||
{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
|
||||
</Badge>
|
||||
)}
|
||||
|
@ -301,28 +341,45 @@ export default function ConnectorsPage() {
|
|||
</CardContent>
|
||||
|
||||
<CardFooter className="mt-auto pt-2">
|
||||
{connector.status === 'available' && (
|
||||
<Link href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`} className="w-full">
|
||||
<Button variant="default" className="w-full group">
|
||||
{connector.status === "available" && (
|
||||
<Link
|
||||
href={`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`}
|
||||
className="w-full"
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full group"
|
||||
>
|
||||
<span>Connect</span>
|
||||
<motion.div
|
||||
className="ml-1"
|
||||
initial={{ x: 0 }}
|
||||
whileHover={{ x: 3 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 10 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 10,
|
||||
}}
|
||||
>
|
||||
<IconChevronRight className="h-4 w-4" />
|
||||
</motion.div>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{connector.status === 'coming-soon' && (
|
||||
<Button variant="outline" disabled className="w-full opacity-70">
|
||||
{connector.status === "coming-soon" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className="w-full opacity-70"
|
||||
>
|
||||
Coming Soon
|
||||
</Button>
|
||||
)}
|
||||
{connector.status === 'connected' && (
|
||||
<Button variant="outline" className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950">
|
||||
{connector.status === "connected" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-green-500 text-green-600 hover:bg-green-50 dark:hover:bg-green-950"
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -103,6 +103,7 @@ type DocumentType =
|
|||
| "YOUTUBE_VIDEO"
|
||||
| "GITHUB_CONNECTOR"
|
||||
| "LINEAR_CONNECTOR"
|
||||
| "JIRA_CONNECTOR"
|
||||
| "DISCORD_CONNECTOR";
|
||||
|
||||
/**
|
||||
|
@ -982,12 +983,14 @@ const ChatPage = () => {
|
|||
if (!message.annotations) return null;
|
||||
|
||||
// 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") {
|
||||
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
|
||||
return terminalInfoAnnotations.map((item: any, idx: number) => (
|
||||
|
@ -1328,29 +1331,41 @@ const ChatPage = () => {
|
|||
}
|
||||
|
||||
// Fallback to the message content if no ANSWER annotation is available
|
||||
return <MarkdownViewer
|
||||
return (
|
||||
<MarkdownViewer
|
||||
content={message.content}
|
||||
getCitationSource={(id) => getCitationSource(id, index)}
|
||||
getCitationSource={(id) =>
|
||||
getCitationSource(id, index)
|
||||
}
|
||||
type="ai"
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Further Questions Section */}
|
||||
{message.annotations && (() => {
|
||||
{message.annotations &&
|
||||
(() => {
|
||||
// Get all FURTHER_QUESTIONS annotations
|
||||
const furtherQuestionsAnnotations = (message.annotations as any[])
|
||||
.filter(a => a.type === 'FURTHER_QUESTIONS');
|
||||
const furtherQuestionsAnnotations = (
|
||||
message.annotations as any[]
|
||||
).filter((a) => a.type === "FURTHER_QUESTIONS");
|
||||
|
||||
// Get the latest FURTHER_QUESTIONS annotation
|
||||
const latestFurtherQuestions = furtherQuestionsAnnotations.length > 0
|
||||
? furtherQuestionsAnnotations[furtherQuestionsAnnotations.length - 1]
|
||||
const latestFurtherQuestions =
|
||||
furtherQuestionsAnnotations.length > 0
|
||||
? furtherQuestionsAnnotations[
|
||||
furtherQuestionsAnnotations.length - 1
|
||||
]
|
||||
: null;
|
||||
|
||||
// Only render if we have questions
|
||||
if (!latestFurtherQuestions?.content || latestFurtherQuestions.content.length === 0) {
|
||||
if (
|
||||
!latestFurtherQuestions?.content ||
|
||||
latestFurtherQuestions.content.length === 0
|
||||
) {
|
||||
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="flex items-center justify-between">
|
||||
<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">
|
||||
<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
|
||||
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>
|
||||
Follow-up Questions
|
||||
</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1387,7 +1413,8 @@ const ChatPage = () => {
|
|||
{/* Scrollable container */}
|
||||
<div className="overflow-x-auto scrollbar-hover">
|
||||
<div className="flex gap-2 py-1 px-6">
|
||||
{furtherQuestions.map((question: any, qIndex: number) => (
|
||||
{furtherQuestions.map(
|
||||
(question: any, qIndex: number) => (
|
||||
<Button
|
||||
key={question.id || qIndex}
|
||||
variant="outline"
|
||||
|
@ -1396,18 +1423,26 @@ const ChatPage = () => {
|
|||
onClick={() => {
|
||||
// Set the input value and submit
|
||||
handleInputChange({
|
||||
target: { value: question.question }
|
||||
target: {
|
||||
value: question.question,
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>);
|
||||
|
||||
// Small delay to ensure input is updated, then submit
|
||||
setTimeout(() => {
|
||||
const form = document.querySelector('form') as HTMLFormElement;
|
||||
if (form && status === 'ready') {
|
||||
const form =
|
||||
document.querySelector(
|
||||
"form",
|
||||
) as HTMLFormElement;
|
||||
if (
|
||||
form &&
|
||||
status === "ready"
|
||||
) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
}, 50);
|
||||
}}
|
||||
disabled={status !== 'ready'}
|
||||
disabled={status !== "ready"}
|
||||
>
|
||||
<span className="text-foreground group-hover:text-primary transition-colors">
|
||||
{question.question}
|
||||
|
@ -1427,7 +1462,8 @@ const ChatPage = () => {
|
|||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
Plus,
|
||||
|
@ -12,77 +12,98 @@ import {
|
|||
Webhook,
|
||||
MessageCircle,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { IconBrandNotion, IconBrandSlack, IconBrandYoutube, IconBrandGithub, IconLayoutKanban, IconLinkPlus, IconBrandDiscord } from "@tabler/icons-react";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Connector, ResearchMode } from './types';
|
||||
} from "lucide-react";
|
||||
import {
|
||||
IconBrandNotion,
|
||||
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
|
||||
export const getConnectorIcon = (connectorType: string) => {
|
||||
const iconProps = { className: "h-4 w-4" };
|
||||
|
||||
switch (connectorType) {
|
||||
case 'LINKUP_API':
|
||||
case "LINKUP_API":
|
||||
return <IconLinkPlus {...iconProps} />;
|
||||
case 'LINEAR_CONNECTOR':
|
||||
case "LINEAR_CONNECTOR":
|
||||
return <IconLayoutKanban {...iconProps} />;
|
||||
case 'GITHUB_CONNECTOR':
|
||||
case "GITHUB_CONNECTOR":
|
||||
return <IconBrandGithub {...iconProps} />;
|
||||
case 'YOUTUBE_VIDEO':
|
||||
case "YOUTUBE_VIDEO":
|
||||
return <IconBrandYoutube {...iconProps} />;
|
||||
case 'CRAWLED_URL':
|
||||
case "CRAWLED_URL":
|
||||
return <Globe {...iconProps} />;
|
||||
case 'FILE':
|
||||
case "FILE":
|
||||
return <File {...iconProps} />;
|
||||
case 'EXTENSION':
|
||||
case "EXTENSION":
|
||||
return <Webhook {...iconProps} />;
|
||||
case 'SERPER_API':
|
||||
case 'TAVILY_API':
|
||||
case "SERPER_API":
|
||||
case "TAVILY_API":
|
||||
return <Link {...iconProps} />;
|
||||
case 'SLACK_CONNECTOR':
|
||||
case "SLACK_CONNECTOR":
|
||||
return <IconBrandSlack {...iconProps} />;
|
||||
case 'NOTION_CONNECTOR':
|
||||
case "NOTION_CONNECTOR":
|
||||
return <IconBrandNotion {...iconProps} />;
|
||||
case 'DISCORD_CONNECTOR':
|
||||
case "DISCORD_CONNECTOR":
|
||||
return <IconBrandDiscord {...iconProps} />;
|
||||
case 'DEEP':
|
||||
case "JIRA_CONNECTOR":
|
||||
return <IconTicket {...iconProps} />;
|
||||
case "DEEP":
|
||||
return <Sparkles {...iconProps} />;
|
||||
case 'DEEPER':
|
||||
case "DEEPER":
|
||||
return <Microscope {...iconProps} />;
|
||||
case 'DEEPEST':
|
||||
case "DEEPEST":
|
||||
return <Telescope {...iconProps} />;
|
||||
default:
|
||||
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',
|
||||
label: 'Q/A',
|
||||
icon: getConnectorIcon('GENERAL')
|
||||
value: "QNA",
|
||||
label: "Q/A",
|
||||
icon: getConnectorIcon("GENERAL"),
|
||||
},
|
||||
{
|
||||
value: 'REPORT_GENERAL',
|
||||
label: 'General',
|
||||
icon: getConnectorIcon('GENERAL')
|
||||
value: "REPORT_GENERAL",
|
||||
label: "General",
|
||||
icon: getConnectorIcon("GENERAL"),
|
||||
},
|
||||
{
|
||||
value: 'REPORT_DEEP',
|
||||
label: 'Deep',
|
||||
icon: getConnectorIcon('DEEP')
|
||||
value: "REPORT_DEEP",
|
||||
label: "Deep",
|
||||
icon: getConnectorIcon("DEEP"),
|
||||
},
|
||||
{
|
||||
value: 'REPORT_DEEPER',
|
||||
label: 'Deeper',
|
||||
icon: getConnectorIcon('DEEPER')
|
||||
value: "REPORT_DEEPER",
|
||||
label: "Deeper",
|
||||
icon: getConnectorIcon("DEEPER"),
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
className="w-4 h-4 rounded-full flex items-center justify-center bg-muted border border-background"
|
||||
style={{ zIndex: 10 - index }}
|
||||
|
@ -109,15 +130,21 @@ type ConnectorButtonProps = {
|
|||
/**
|
||||
* 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 selectedCount = selectedConnectors.length;
|
||||
const progressPercentage = (selectedCount / totalConnectors) * 100;
|
||||
|
||||
// Get the name of a single selected connector
|
||||
const getSingleConnectorName = () => {
|
||||
const connector = connectorSources.find(c => c.type === selectedConnectors[0]);
|
||||
return connector?.name || '';
|
||||
const connector = connectorSources.find(
|
||||
(c) => c.type === selectedConnectors[0],
|
||||
);
|
||||
return connector?.name || "";
|
||||
};
|
||||
|
||||
// Get display text based on selection count
|
||||
|
@ -158,14 +185,18 @@ export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources
|
|||
variant="outline"
|
||||
className="h-8 px-2 text-xs font-medium rounded-md border-border relative overflow-hidden group"
|
||||
onClick={onClick}
|
||||
aria-label={selectedCount === 0 ? "Select Connectors" : `${selectedCount} connectors selected`}
|
||||
aria-label={
|
||||
selectedCount === 0
|
||||
? "Select Connectors"
|
||||
: `${selectedCount} connectors selected`
|
||||
}
|
||||
>
|
||||
{/* Progress indicator */}
|
||||
<div
|
||||
className="absolute bottom-0 left-0 h-1 bg-primary"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
transition: 'width 0.3s ease'
|
||||
transition: "width 0.3s ease",
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -183,29 +214,32 @@ type ResearchModeControlProps = {
|
|||
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
|
||||
const isQnaMode = value === 'QNA';
|
||||
const isReportMode = value.startsWith('REPORT_');
|
||||
const isQnaMode = value === "QNA";
|
||||
const isReportMode = value.startsWith("REPORT_");
|
||||
|
||||
// Get the current report sub-mode
|
||||
const getCurrentReportMode = () => {
|
||||
if (!isReportMode) return 'GENERAL';
|
||||
return value.replace('REPORT_', '') as 'GENERAL' | 'DEEP' | 'DEEPER';
|
||||
if (!isReportMode) return "GENERAL";
|
||||
return value.replace("REPORT_", "") as "GENERAL" | "DEEP" | "DEEPER";
|
||||
};
|
||||
|
||||
const reportSubOptions = [
|
||||
{ value: 'GENERAL', label: 'General', icon: getConnectorIcon('GENERAL') },
|
||||
{ value: 'DEEP', label: 'Deep', icon: getConnectorIcon('DEEP') },
|
||||
{ value: 'DEEPER', label: 'Deeper', icon: getConnectorIcon('DEEPER') },
|
||||
{ value: "GENERAL", label: "General", icon: getConnectorIcon("GENERAL") },
|
||||
{ value: "DEEP", label: "Deep", icon: getConnectorIcon("DEEP") },
|
||||
{ value: "DEEPER", label: "Deeper", icon: getConnectorIcon("DEEPER") },
|
||||
];
|
||||
|
||||
const handleModeToggle = (mode: 'QNA' | 'REPORT') => {
|
||||
if (mode === 'QNA') {
|
||||
onChange('QNA');
|
||||
const handleModeToggle = (mode: "QNA" | "REPORT") => {
|
||||
if (mode === "QNA") {
|
||||
onChange("QNA");
|
||||
} else {
|
||||
// Default to GENERAL for Report mode
|
||||
onChange('REPORT_GENERAL');
|
||||
onChange("REPORT_GENERAL");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -220,10 +254,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
|||
<button
|
||||
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||
isQnaMode
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => handleModeToggle('QNA')}
|
||||
onClick={() => handleModeToggle("QNA")}
|
||||
aria-pressed={isQnaMode}
|
||||
>
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
|
@ -232,10 +266,10 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
|||
<button
|
||||
className={`flex h-full items-center gap-1 px-3 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||
isReportMode
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => handleModeToggle('REPORT')}
|
||||
onClick={() => handleModeToggle("REPORT")}
|
||||
aria-pressed={isReportMode}
|
||||
>
|
||||
<FileText className="h-3 w-3" />
|
||||
|
@ -251,8 +285,8 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
|||
key={option.value}
|
||||
className={`flex h-full items-center gap-1 px-2 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||
getCurrentReportMode() === option.value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted text-muted-foreground hover:text-foreground'
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => handleReportSubModeChange(option.value)}
|
||||
aria-pressed={getCurrentReportMode() === option.value}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
// Helper function to get connector type display name
|
||||
export const getConnectorTypeDisplay = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
"SERPER_API": "Serper API",
|
||||
"TAVILY_API": "Tavily API",
|
||||
"SLACK_CONNECTOR": "Slack",
|
||||
"NOTION_CONNECTOR": "Notion",
|
||||
"GITHUB_CONNECTOR": "GitHub",
|
||||
"LINEAR_CONNECTOR": "Linear",
|
||||
"DISCORD_CONNECTOR": "Discord",
|
||||
"LINKUP_API": "Linkup",
|
||||
SERPER_API: "Serper API",
|
||||
TAVILY_API: "Tavily API",
|
||||
SLACK_CONNECTOR: "Slack",
|
||||
NOTION_CONNECTOR: "Notion",
|
||||
GITHUB_CONNECTOR: "GitHub",
|
||||
LINEAR_CONNECTOR: "Linear",
|
||||
JIRA_CONNECTOR: "Jira",
|
||||
DISCORD_CONNECTOR: "Discord",
|
||||
LINKUP_API: "Linkup",
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue