VIBN Frontend for Coolify deployment
This commit is contained in:
385
components/ai/collector-actions.tsx
Normal file
385
components/ai/collector-actions.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user