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)
|
- 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>
|
||||||
|
|
|
@ -9,12 +9,12 @@ import { ArrowLeft, Check, Loader2, Github } from "lucide-react";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
// Import Utils, Types, Hook, and Components
|
// Import Utils, Types, Hook, and Components
|
||||||
|
@ -27,201 +27,220 @@ import { EditSimpleTokenForm } from "@/components/editConnector/EditSimpleTokenF
|
||||||
import { getConnectorIcon } from "@/components/chat";
|
import { getConnectorIcon } from "@/components/chat";
|
||||||
|
|
||||||
export default function EditConnectorPage() {
|
export default function EditConnectorPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
// Ensure connectorId is parsed safely
|
// Ensure connectorId is parsed safely
|
||||||
const connectorIdParam = params.connector_id as string;
|
const connectorIdParam = params.connector_id as string;
|
||||||
const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN;
|
const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN;
|
||||||
|
|
||||||
// Use the custom hook to manage state and logic
|
// Use the custom hook to manage state and logic
|
||||||
const {
|
const {
|
||||||
connectorsLoading,
|
connectorsLoading,
|
||||||
connector,
|
connector,
|
||||||
isSaving,
|
isSaving,
|
||||||
editForm,
|
editForm,
|
||||||
patForm, // Needed for GitHub child component
|
patForm, // Needed for GitHub child component
|
||||||
handleSaveChanges,
|
handleSaveChanges,
|
||||||
// GitHub specific props for the child component
|
// GitHub specific props for the child component
|
||||||
editMode,
|
editMode,
|
||||||
setEditMode, // Pass down if needed by GitHub component
|
setEditMode, // Pass down if needed by GitHub component
|
||||||
originalPat,
|
originalPat,
|
||||||
currentSelectedRepos,
|
currentSelectedRepos,
|
||||||
fetchedRepos,
|
fetchedRepos,
|
||||||
setFetchedRepos,
|
setFetchedRepos,
|
||||||
newSelectedRepos,
|
newSelectedRepos,
|
||||||
setNewSelectedRepos,
|
setNewSelectedRepos,
|
||||||
isFetchingRepos,
|
isFetchingRepos,
|
||||||
handleFetchRepositories,
|
handleFetchRepositories,
|
||||||
handleRepoSelectionChange,
|
handleRepoSelectionChange,
|
||||||
} = useConnectorEditPage(connectorId, searchSpaceId);
|
} = useConnectorEditPage(connectorId, searchSpaceId);
|
||||||
|
|
||||||
// Redirect if connectorId is not a valid number after parsing
|
// Redirect if connectorId is not a valid number after parsing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNaN(connectorId)) {
|
if (isNaN(connectorId)) {
|
||||||
toast.error("Invalid Connector ID.");
|
toast.error("Invalid Connector ID.");
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
}
|
}
|
||||||
}, [connectorId, router, searchSpaceId]);
|
}, [connectorId, router, searchSpaceId]);
|
||||||
|
|
||||||
// Loading State
|
// Loading State
|
||||||
if (connectorsLoading || !connector) {
|
if (connectorsLoading || !connector) {
|
||||||
// Handle NaN case before showing skeleton
|
// Handle NaN case before showing skeleton
|
||||||
if (isNaN(connectorId)) return null;
|
if (isNaN(connectorId)) return null;
|
||||||
return <EditConnectorLoadingSkeleton />;
|
return <EditConnectorLoadingSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main Render using data/handlers from the hook
|
// Main Render using data/handlers from the hook
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 max-w-3xl">
|
<div className="container mx-auto py-8 max-w-3xl">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors`)}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Connectors
|
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Connectors
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
{getConnectorIcon(connector.connector_type)}
|
{getConnectorIcon(connector.connector_type)}
|
||||||
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Modify connector name and configuration.
|
Modify connector name and configuration.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<Form {...editForm}>
|
<Form {...editForm}>
|
||||||
{/* Pass hook's handleSaveChanges */}
|
{/* Pass hook's handleSaveChanges */}
|
||||||
<form
|
<form
|
||||||
onSubmit={editForm.handleSubmit(handleSaveChanges)}
|
onSubmit={editForm.handleSubmit(handleSaveChanges)}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Pass form control from hook */}
|
{/* Pass form control from hook */}
|
||||||
<EditConnectorNameForm control={editForm.control} />
|
<EditConnectorNameForm control={editForm.control} />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold">Configuration</h3>
|
<h3 className="text-lg font-semibold">Configuration</h3>
|
||||||
|
|
||||||
{/* == GitHub == */}
|
{/* == GitHub == */}
|
||||||
{connector.connector_type === "GITHUB_CONNECTOR" && (
|
{connector.connector_type === "GITHUB_CONNECTOR" && (
|
||||||
<EditGitHubConnectorConfig
|
<EditGitHubConnectorConfig
|
||||||
// Pass relevant state and handlers from hook
|
// Pass relevant state and handlers from hook
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
setEditMode={setEditMode} // Pass setter if child manages mode
|
setEditMode={setEditMode} // Pass setter if child manages mode
|
||||||
originalPat={originalPat}
|
originalPat={originalPat}
|
||||||
currentSelectedRepos={currentSelectedRepos}
|
currentSelectedRepos={currentSelectedRepos}
|
||||||
fetchedRepos={fetchedRepos}
|
fetchedRepos={fetchedRepos}
|
||||||
newSelectedRepos={newSelectedRepos}
|
newSelectedRepos={newSelectedRepos}
|
||||||
isFetchingRepos={isFetchingRepos}
|
isFetchingRepos={isFetchingRepos}
|
||||||
patForm={patForm}
|
patForm={patForm}
|
||||||
handleFetchRepositories={handleFetchRepositories}
|
handleFetchRepositories={handleFetchRepositories}
|
||||||
handleRepoSelectionChange={handleRepoSelectionChange}
|
handleRepoSelectionChange={handleRepoSelectionChange}
|
||||||
setNewSelectedRepos={setNewSelectedRepos}
|
setNewSelectedRepos={setNewSelectedRepos}
|
||||||
setFetchedRepos={setFetchedRepos}
|
setFetchedRepos={setFetchedRepos}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Slack == */}
|
{/* == Slack == */}
|
||||||
{connector.connector_type === "SLACK_CONNECTOR" && (
|
{connector.connector_type === "SLACK_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="SLACK_BOT_TOKEN"
|
fieldName="SLACK_BOT_TOKEN"
|
||||||
fieldLabel="Slack Bot Token"
|
fieldLabel="Slack Bot Token"
|
||||||
fieldDescription="Update the Slack Bot Token if needed."
|
fieldDescription="Update the Slack Bot Token if needed."
|
||||||
placeholder="Begins with xoxb-..."
|
placeholder="Begins with xoxb-..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Notion == */}
|
{/* == Notion == */}
|
||||||
{connector.connector_type === "NOTION_CONNECTOR" && (
|
{connector.connector_type === "NOTION_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="NOTION_INTEGRATION_TOKEN"
|
fieldName="NOTION_INTEGRATION_TOKEN"
|
||||||
fieldLabel="Notion Integration Token"
|
fieldLabel="Notion Integration Token"
|
||||||
fieldDescription="Update the Notion Integration Token if needed."
|
fieldDescription="Update the Notion Integration Token if needed."
|
||||||
placeholder="Begins with secret_..."
|
placeholder="Begins with secret_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Serper == */}
|
{/* == Serper == */}
|
||||||
{connector.connector_type === "SERPER_API" && (
|
{connector.connector_type === "SERPER_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="SERPER_API_KEY"
|
fieldName="SERPER_API_KEY"
|
||||||
fieldLabel="Serper API Key"
|
fieldLabel="Serper API Key"
|
||||||
fieldDescription="Update the Serper API Key if needed."
|
fieldDescription="Update the Serper API Key if needed."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* == Tavily == */}
|
{/* == Tavily == */}
|
||||||
{connector.connector_type === "TAVILY_API" && (
|
{connector.connector_type === "TAVILY_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="TAVILY_API_KEY"
|
fieldName="TAVILY_API_KEY"
|
||||||
fieldLabel="Tavily API Key"
|
fieldLabel="Tavily API Key"
|
||||||
fieldDescription="Update the Tavily API Key if needed."
|
fieldDescription="Update the Tavily API Key if needed."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Linear == */}
|
{/* == Linear == */}
|
||||||
{connector.connector_type === "LINEAR_CONNECTOR" && (
|
{connector.connector_type === "LINEAR_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="LINEAR_API_KEY"
|
fieldName="LINEAR_API_KEY"
|
||||||
fieldLabel="Linear API Key"
|
fieldLabel="Linear API Key"
|
||||||
fieldDescription="Update your Linear API Key if needed."
|
fieldDescription="Update your Linear API Key if needed."
|
||||||
placeholder="Begins with lin_api_..."
|
placeholder="Begins with lin_api_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* == Linkup == */}
|
{/* == Jira == */}
|
||||||
{connector.connector_type === "LINKUP_API" && (
|
{connector.connector_type === "JIRA_CONNECTOR" && (
|
||||||
<EditSimpleTokenForm
|
<div className="space-y-4">
|
||||||
control={editForm.control}
|
<EditSimpleTokenForm
|
||||||
fieldName="LINKUP_API_KEY"
|
control={editForm.control}
|
||||||
fieldLabel="Linkup API Key"
|
fieldName="JIRA_BASE_URL"
|
||||||
fieldDescription="Update your Linkup API Key if needed."
|
fieldLabel="Jira Base URL"
|
||||||
placeholder="Begins with linkup_..."
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* == Discord == */}
|
{/* == Linkup == */}
|
||||||
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
{connector.connector_type === "LINKUP_API" && (
|
||||||
<EditSimpleTokenForm
|
<EditSimpleTokenForm
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
fieldName="DISCORD_BOT_TOKEN"
|
fieldName="LINKUP_API_KEY"
|
||||||
fieldLabel="Discord Bot Token"
|
fieldLabel="Linkup API Key"
|
||||||
fieldDescription="Update the Discord Bot Token if needed."
|
fieldDescription="Update your Linkup API Key if needed."
|
||||||
placeholder="Bot token..."
|
placeholder="Begins with linkup_..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</CardContent>
|
{/* == Discord == */}
|
||||||
<CardFooter className="border-t pt-6">
|
{connector.connector_type === "DISCORD_CONNECTOR" && (
|
||||||
<Button
|
<EditSimpleTokenForm
|
||||||
type="submit"
|
control={editForm.control}
|
||||||
disabled={isSaving}
|
fieldName="DISCORD_BOT_TOKEN"
|
||||||
className="w-full sm:w-auto"
|
fieldLabel="Discord Bot Token"
|
||||||
>
|
fieldDescription="Update the Discord Bot Token if needed."
|
||||||
{isSaving ? (
|
placeholder="Bot token..."
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
/>
|
||||||
) : (
|
)}
|
||||||
<Check className="mr-2 h-4 w-4" />
|
</CardContent>
|
||||||
)}
|
<CardFooter className="border-t pt-6">
|
||||||
Save Changes
|
<Button
|
||||||
</Button>
|
type="submit"
|
||||||
</CardFooter>
|
disabled={isSaving}
|
||||||
</form>
|
className="w-full sm:w-auto"
|
||||||
</Form>
|
>
|
||||||
</Card>
|
{isSaving ? (
|
||||||
</motion.div>
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
</div>
|
) : (
|
||||||
);
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -67,9 +68,11 @@ export default function EditConnectorPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
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,24 +88,24 @@ 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);
|
||||||
|
|
||||||
// Check if connector type is supported
|
// Check if connector type is supported
|
||||||
const apiKeyField = getApiKeyFieldName(currentConnector.connector_type);
|
const apiKeyField = getApiKeyFieldName(currentConnector.connector_type);
|
||||||
if (apiKeyField) {
|
if (apiKeyField) {
|
||||||
|
@ -115,7 +118,7 @@ export default function EditConnectorPage() {
|
||||||
toast.error("This connector type is not supported for editing");
|
toast.error("This connector type is not supported for editing");
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else if (!isLoading && connectors.length > 0) {
|
} else if (!isLoading && connectors.length > 0) {
|
||||||
// If connectors are loaded but this one isn't found
|
// If connectors are loaded but this one isn't found
|
||||||
|
@ -127,11 +130,11 @@ export default function EditConnectorPage() {
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const onSubmit = async (values: ApiConnectorFormValues) => {
|
const onSubmit = async (values: ApiConnectorFormValues) => {
|
||||||
if (!connector) return;
|
if (!connector) return;
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const apiKeyField = getApiKeyFieldName(connector.connector_type);
|
const apiKeyField = getApiKeyFieldName(connector.connector_type);
|
||||||
|
|
||||||
// Only update the API key if a new one was provided
|
// Only update the API key if a new one was provided
|
||||||
const updatedConfig = { ...connector.config };
|
const updatedConfig = { ...connector.config };
|
||||||
if (values.api_key) {
|
if (values.api_key) {
|
||||||
|
@ -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"
|
||||||
|
@ -227,10 +238,10 @@ export default function EditConnectorPage() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{connector?.connector_type === "SLACK_CONNECTOR"
|
{connector?.connector_type === "SLACK_CONNECTOR"
|
||||||
? "Slack Bot Token"
|
? "Slack Bot Token"
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
: connector?.connector_type === "NOTION_CONNECTOR"
|
||||||
? "Notion Integration Token"
|
? "Notion Integration Token"
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
: connector?.connector_type === "GITHUB_CONNECTOR"
|
||||||
? "GitHub Personal Access Token (PAT)"
|
? "GitHub Personal Access Token (PAT)"
|
||||||
: connector?.connector_type === "LINKUP_API"
|
: connector?.connector_type === "LINKUP_API"
|
||||||
|
@ -238,27 +249,28 @@ export default function EditConnectorPage() {
|
||||||
: "API Key"}
|
: "API Key"}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={
|
placeholder={
|
||||||
connector?.connector_type === "SLACK_CONNECTOR"
|
connector?.connector_type === "SLACK_CONNECTOR"
|
||||||
? "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)"
|
||||||
: "Enter new API key (optional)"
|
: "Enter new API key (optional)"
|
||||||
}
|
}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{connector?.connector_type === "SLACK_CONNECTOR"
|
{connector?.connector_type === "SLACK_CONNECTOR"
|
||||||
? "Enter a new Slack Bot Token or leave blank to keep your existing token."
|
? "Enter a new Slack Bot Token or leave blank to keep your existing token."
|
||||||
: connector?.connector_type === "NOTION_CONNECTOR"
|
: connector?.connector_type === "NOTION_CONNECTOR"
|
||||||
? "Enter a new Notion Integration Token or leave blank to keep your existing token."
|
? "Enter a new Notion Integration Token or leave blank to keep your existing token."
|
||||||
: connector?.connector_type === "GITHUB_CONNECTOR"
|
: connector?.connector_type === "GITHUB_CONNECTOR"
|
||||||
? "Enter a new GitHub PAT or leave blank to keep your existing token."
|
? "Enter a new GitHub PAT or leave blank to keep your existing token."
|
||||||
: connector?.connector_type === "LINKUP_API"
|
: connector?.connector_type === "LINKUP_API"
|
||||||
|
@ -271,8 +283,8 @@ export default function EditConnectorPage() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
|
@ -296,4 +308,4 @@ export default function EditConnectorPage() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
"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,43 +177,49 @@ const staggerContainer = {
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
staggerChildren: 0.1
|
staggerChildren: 0.1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardVariants = {
|
const cardVariants = {
|
||||||
hidden: { opacity: 0, y: 20 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
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:
|
||||||
transition: {
|
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
||||||
|
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],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -205,9 +228,9 @@ export default function ConnectorsPage() {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
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,18 +238,19 @@ 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>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="space-y-8"
|
className="space-y-8"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={staggerContainer}
|
variants={staggerContainer}
|
||||||
>
|
>
|
||||||
{connectorCategories.map((category) => (
|
{connectorCategories.map((category) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={category.id}
|
key={category.id}
|
||||||
variants={fadeIn}
|
variants={fadeIn}
|
||||||
className="rounded-lg border bg-card text-card-foreground shadow-sm"
|
className="rounded-lg border bg-card text-card-foreground shadow-sm"
|
||||||
|
@ -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" />
|
||||||
|
@ -250,10 +282,10 @@ export default function ConnectorsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 p-4"
|
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 p-4"
|
||||||
variants={staggerContainer}
|
variants={staggerContainer}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
@ -279,50 +311,75 @@ 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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="pb-4">
|
<CardContent className="pb-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{connector.description}
|
{connector.description}
|
||||||
</p>
|
</p>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Globe,
|
Globe,
|
||||||
|
@ -12,78 +12,99 @@ 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 = ({
|
||||||
<div
|
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"
|
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,24 +130,30 @@ 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
|
||||||
const getDisplayText = () => {
|
const getDisplayText = () => {
|
||||||
if (selectedCount === totalConnectors) return "All Connectors";
|
if (selectedCount === totalConnectors) return "All Connectors";
|
||||||
if (selectedCount === 1) return getSingleConnectorName();
|
if (selectedCount === 1) return getSingleConnectorName();
|
||||||
return `${selectedCount} Connectors`;
|
return `${selectedCount} Connectors`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render the empty state (no connectors selected)
|
// Render the empty state (no connectors selected)
|
||||||
const renderEmptyState = () => (
|
const renderEmptyState = () => (
|
||||||
<>
|
<>
|
||||||
|
@ -134,7 +161,7 @@ export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources
|
||||||
<span className="text-muted-foreground">Select Connectors</span>
|
<span className="text-muted-foreground">Select Connectors</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render the selected connectors preview
|
// Render the selected connectors preview
|
||||||
const renderSelectedConnectors = () => (
|
const renderSelectedConnectors = () => (
|
||||||
<>
|
<>
|
||||||
|
@ -143,32 +170,36 @@ export const ConnectorButton = ({ selectedConnectors, onClick, connectorSources
|
||||||
{selectedConnectors.slice(0, 3).map((type, index) => (
|
{selectedConnectors.slice(0, 3).map((type, index) => (
|
||||||
<ConnectorIcon key={type} type={type} index={index} />
|
<ConnectorIcon key={type} type={type} index={index} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Show count indicator if more than 3 connectors are selected */}
|
{/* Show count indicator if more than 3 connectors are selected */}
|
||||||
{selectedCount > 3 && <ConnectorCountBadge count={selectedCount - 3} />}
|
{selectedCount > 3 && <ConnectorCountBadge count={selectedCount - 3} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Display text */}
|
{/* Display text */}
|
||||||
<span className="font-medium">{getDisplayText()}</span>
|
<span className="font-medium">{getDisplayText()}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
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",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 z-10 relative">
|
<div className="flex items-center gap-1.5 z-10 relative">
|
||||||
{selectedCount === 0 ? renderEmptyState() : renderSelectedConnectors()}
|
{selectedCount === 0 ? renderEmptyState() : renderSelectedConnectors()}
|
||||||
<ChevronDown className="h-3 w-3 ml-0.5 text-muted-foreground opacity-70" />
|
<ChevronDown className="h-3 w-3 ml-0.5 text-muted-foreground opacity-70" />
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,11 +253,11 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
<div className="flex h-8 rounded-md border border-border overflow-hidden">
|
<div className="flex h-8 rounded-md border border-border overflow-hidden">
|
||||||
<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" />
|
||||||
|
@ -231,11 +265,11 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
</button>
|
</button>
|
||||||
<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" />
|
||||||
|
@ -250,9 +284,9 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
<button
|
<button
|
||||||
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}
|
||||||
|
@ -265,4 +299,4 @@ export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProp
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue