Files
vibn-frontend/components/ai/collector-actions.tsx

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>
</>
);
}