386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Upload, Github, Plug, FileText, Download, Copy, Check } from "lucide-react";
|
|
import { useState, useRef } from "react";
|
|
import { auth } from "@/lib/firebase/config";
|
|
import { toast } from "sonner";
|
|
import { GitHubRepoPicker } from "./github-repo-picker";
|
|
import { CursorIcon } from "@/components/icons/custom-icons";
|
|
|
|
interface CollectorActionsProps {
|
|
projectId: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function CollectorActions({ projectId, className }: CollectorActionsProps) {
|
|
const [uploading, setUploading] = useState(false);
|
|
const [showGithubPicker, setShowGithubPicker] = useState(false);
|
|
const [showPasteDialog, setShowPasteDialog] = useState(false);
|
|
const [showCursorImportDialog, setShowCursorImportDialog] = useState(false);
|
|
const [pasteTitle, setPasteTitle] = useState("");
|
|
const [pasteContent, setPasteContent] = useState("");
|
|
const [isPasting, setIsPasting] = useState(false);
|
|
const [copiedConfig, setCopiedConfig] = useState(false);
|
|
const [copiedCommand, setCopiedCommand] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = event.target.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
setUploading(true);
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) {
|
|
toast.error("Please sign in to upload files");
|
|
return;
|
|
}
|
|
|
|
const token = await user.getIdToken();
|
|
|
|
for (const file of Array.from(files)) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/knowledge/upload-document`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to upload ${file.name}`);
|
|
}
|
|
}
|
|
|
|
toast.success(`Uploaded ${files.length} file(s)`);
|
|
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = "";
|
|
}
|
|
} catch (error) {
|
|
console.error("Upload error:", error);
|
|
toast.error("Failed to upload files");
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
};
|
|
|
|
const handleExtensionClick = () => {
|
|
window.open("https://chrome.google.com/webstore", "_blank");
|
|
toast.info("Install the Vibn browser extension and link it to this project");
|
|
};
|
|
|
|
const handleCopyConfig = () => {
|
|
const vibnConfig = {
|
|
projectId: projectId,
|
|
version: "1.0.0"
|
|
};
|
|
|
|
const content = JSON.stringify(vibnConfig, null, 2);
|
|
navigator.clipboard.writeText(content);
|
|
setCopiedConfig(true);
|
|
toast.success("Configuration copied to clipboard!");
|
|
setTimeout(() => setCopiedConfig(false), 2000);
|
|
};
|
|
|
|
const handleCopyCommand = () => {
|
|
navigator.clipboard.writeText("Vibn: Import Historical Conversations");
|
|
setCopiedCommand(true);
|
|
toast.success("Command copied to clipboard!");
|
|
setTimeout(() => setCopiedCommand(false), 2000);
|
|
};
|
|
|
|
const handlePasteSubmit = async () => {
|
|
if (!pasteTitle.trim() || !pasteContent.trim()) {
|
|
toast.error("Please provide both title and content");
|
|
return;
|
|
}
|
|
|
|
setIsPasting(true);
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) {
|
|
toast.error("Please sign in to save content");
|
|
return;
|
|
}
|
|
|
|
const token = await user.getIdToken();
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/knowledge/import-ai-chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
title: pasteTitle,
|
|
transcript: pasteContent,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to import chat");
|
|
}
|
|
|
|
toast.success("AI chat imported successfully!");
|
|
setShowPasteDialog(false);
|
|
setPasteTitle("");
|
|
setPasteContent("");
|
|
} catch (error) {
|
|
console.error("Paste error:", error);
|
|
toast.error("Failed to import chat");
|
|
} finally {
|
|
setIsPasting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept=".txt,.md,.json,.pdf,.doc,.docx"
|
|
onChange={handleFileSelect}
|
|
className="hidden"
|
|
/>
|
|
|
|
{/* Upload Documents */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-2 px-3"
|
|
onClick={() => fileInputRef.current?.click()}
|
|
disabled={uploading}
|
|
>
|
|
<Upload className="h-4 w-4 mr-2 flex-shrink-0" />
|
|
<span className="text-xs">
|
|
{uploading ? "Uploading..." : "Upload Documents"}
|
|
</span>
|
|
</Button>
|
|
|
|
{/* Connect GitHub */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-2 px-3"
|
|
onClick={() => setShowGithubPicker(true)}
|
|
>
|
|
<Github className="h-4 w-4 mr-2 flex-shrink-0" />
|
|
<span className="text-xs">Connect GitHub</span>
|
|
</Button>
|
|
|
|
{/* Get Extension */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-2 px-3"
|
|
onClick={handleExtensionClick}
|
|
>
|
|
<Plug className="h-4 w-4 mr-2 flex-shrink-0" />
|
|
<span className="text-xs">Get Extension</span>
|
|
</Button>
|
|
|
|
{/* Paste AI Chat */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-2 px-3"
|
|
onClick={() => setShowPasteDialog(true)}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2 flex-shrink-0" />
|
|
<span className="text-xs">Paste AI Chat</span>
|
|
</Button>
|
|
|
|
{/* Import Cursor History */}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start h-auto py-2 px-3"
|
|
onClick={() => setShowCursorImportDialog(true)}
|
|
>
|
|
<CursorIcon className="h-4 w-4 mr-2 flex-shrink-0" />
|
|
<span className="text-xs">Import Cursor History</span>
|
|
</Button>
|
|
|
|
{/* GitHub Picker Dialog */}
|
|
{showGithubPicker && (
|
|
<GitHubRepoPicker
|
|
projectId={projectId}
|
|
onClose={() => setShowGithubPicker(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Paste AI Chat Dialog */}
|
|
<Dialog open={showPasteDialog} onOpenChange={setShowPasteDialog}>
|
|
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle>Import AI Chat</DialogTitle>
|
|
<DialogDescription>
|
|
Paste a conversation from ChatGPT, Claude, or any other AI tool
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 overflow-y-auto space-y-4">
|
|
<div>
|
|
<Label htmlFor="paste-title">Title</Label>
|
|
<Input
|
|
id="paste-title"
|
|
placeholder="e.g., Product Vision Discussion"
|
|
value={pasteTitle}
|
|
onChange={(e) => setPasteTitle(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<Label htmlFor="paste-content">Chat Content</Label>
|
|
<Textarea
|
|
id="paste-content"
|
|
placeholder="Paste your entire conversation here..."
|
|
value={pasteContent}
|
|
onChange={(e) => setPasteContent(e.target.value)}
|
|
className="mt-1 min-h-[300px] font-mono text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowPasteDialog(false)}
|
|
disabled={isPasting}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handlePasteSubmit}
|
|
disabled={isPasting || !pasteTitle.trim() || !pasteContent.trim()}
|
|
>
|
|
{isPasting ? "Importing..." : "Import Chat"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Cursor Import Dialog */}
|
|
<Dialog open={showCursorImportDialog} onOpenChange={setShowCursorImportDialog}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<CursorIcon className="h-5 w-5" />
|
|
Import Cursor Conversation History
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Import all conversations from Cursor related to this project
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg bg-muted p-4 space-y-3">
|
|
<p className="text-sm font-medium">Step 1: Create .vibn file</p>
|
|
<p className="text-xs text-muted-foreground mb-2">
|
|
In your project root directory (where you open Cursor), create a file named <code className="text-xs bg-background px-1 py-0.5 rounded">.vibn</code>
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-xs font-medium">Paste this content into the file:</p>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleCopyConfig}
|
|
className="h-7 gap-2"
|
|
>
|
|
{copiedConfig ? (
|
|
<>
|
|
<Check className="h-3 w-3" />
|
|
Copied
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="h-3 w-3" />
|
|
Copy
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<pre className="text-xs bg-background p-3 rounded border overflow-x-auto">
|
|
<code>{JSON.stringify({ projectId: projectId, version: "1.0.0" }, null, 2)}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-lg bg-muted p-4 space-y-3">
|
|
<p className="text-sm font-medium">Step 2: Reload Cursor window</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
After creating the .vibn file, reload Cursor to register the new configuration:
|
|
</p>
|
|
<ul className="list-disc list-inside space-y-1 text-xs text-muted-foreground ml-2">
|
|
<li>Command Palette → <strong>Developer: Reload Window</strong></li>
|
|
<li>Or: Close and reopen Cursor</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="rounded-lg bg-muted p-4 space-y-3">
|
|
<p className="text-sm font-medium">Step 3: Run import command</p>
|
|
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground ml-2">
|
|
<li>Open Command Palette (Cmd+Shift+P / Ctrl+Shift+P)</li>
|
|
<li>Run this command:</li>
|
|
</ol>
|
|
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<code className="flex-1 text-xs bg-background px-3 py-2 rounded border">
|
|
Vibn: Import Historical Conversations
|
|
</code>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={handleCopyCommand}
|
|
className="h-8"
|
|
>
|
|
{copiedCommand ? (
|
|
<Check className="h-3 w-3" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
|
|
<p className="text-sm text-blue-700 dark:text-blue-400">
|
|
💡 <strong>Note:</strong> The Cursor Monitor extension must be installed and configured with your Vibn API key.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium text-muted-foreground">What gets imported:</p>
|
|
<ul className="text-xs text-muted-foreground space-y-1 ml-4">
|
|
<li>• All chat sessions from the workspace</li>
|
|
<li>• User prompts and AI responses</li>
|
|
<li>• File edit history and context</li>
|
|
<li>• Conversation timestamps and metadata</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowCursorImportDialog(false)}
|
|
>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|
|
|