Plandex / components /NewSessionModal.tsx
AUXteam's picture
Upload folder using huggingface_hub
c59bca4 verified
import React, { useState, useEffect } from 'react';
import { X, Github, Upload, Play, Loader2, GitBranch, Search, AlertTriangle } from 'lucide-react';
import { JulesSource } from '../types';
interface NewSessionModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (config: NewSessionConfig) => Promise<void>;
initialPrompt: string;
sources: JulesSource[];
isLoading: boolean;
githubToken?: string;
githubProfile?: string;
}
export interface NewSessionConfig {
title: string;
prompt: string;
sourceId: string; // The Jules Source Name e.g., "sources/github/..."
githubRepoId: string; // "owner/repo" for HF API
branch: string;
hfImport?: {
spaceId: string;
};
runHfDeploymentCue?: boolean;
}
export const NewSessionModal: React.FC<NewSessionModalProps> = ({
isOpen,
onClose,
onSubmit,
initialPrompt,
sources,
isLoading,
githubToken,
githubProfile
}) => {
const [title, setTitle] = useState('');
const [repoSearch, setRepoSearch] = useState('');
const [isManualTitle, setIsManualTitle] = useState(false);
const [selectedSource, setSelectedSource] = useState<JulesSource | null>(null);
const [branch, setBranch] = useState('main');
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
const [prompt, setPrompt] = useState(initialPrompt);
const [isHfImportEnabled, setIsHfImportEnabled] = useState(false);
const [hfSpaceId, setHfSpaceId] = useState('');
const [runHfDeploymentCue, setRunHfDeploymentCue] = useState(false);
// Update prompt if initialPrompt changes
useEffect(() => {
setPrompt(initialPrompt);
// Auto-generate a title from prompt ONLY if no repo is set
if (initialPrompt && !title && !repoSearch && !isManualTitle) {
setTitle(initialPrompt.slice(0, 30) + (initialPrompt.length > 30 ? '...' : ''));
}
}, [initialPrompt]);
// Handle URL transformation and Auto-Title
useEffect(() => {
let currentRepo = repoSearch;
// URL transformation: https://github.com/owner/repo(.git) -> owner/repo
const githubUrlRegex = /https?:\/\/github\.com\/([^/]+)\/([^/]+)/;
const match = currentRepo.match(githubUrlRegex);
if (match) {
let repoPart = match[2].replace(/\.git$/, '');
const transformed = `${match[1]}/${repoPart}`;
setRepoSearch(transformed);
currentRepo = transformed;
}
// Auto-Title based on repo if title is empty or not manually set
if (!isManualTitle && currentRepo.includes('/')) {
const repoName = currentRepo.split('/').pop() || '';
if (repoName) {
setTitle(repoName.charAt(0).toUpperCase() + repoName.slice(1));
}
}
}, [repoSearch, isManualTitle]);
// Handle Repo Selection and Branch Fetching
useEffect(() => {
const fetchBranches = async () => {
const repoId = selectedSource
? `${selectedSource.githubRepo.owner}/${selectedSource.githubRepo.repo}`
: repoSearch.includes('/') ? repoSearch : null;
if (!repoId) {
setAvailableBranches([]);
return;
}
setIsLoadingBranches(true);
try {
const [owner, repo] = repoId.split('/');
const url = `/api/github/branches?owner=${owner}&repo=${repo}${githubToken ? '&token=' + githubToken : ''}${githubProfile ? '&profile=' + githubProfile : ''}`;
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
const names = data.map((b: any) => b.name);
setAvailableBranches(names);
if (!branch || !names.includes(branch)) {
setBranch(names.find((n: string) => n === 'main' || n === 'master') || names[0] || 'main');
}
} else {
setAvailableBranches([]);
}
} catch (e) {
console.error("Failed to fetch branches", e);
setAvailableBranches([]);
} finally {
setIsLoadingBranches(false);
}
};
const timer = setTimeout(fetchBranches, 500);
return () => clearTimeout(timer);
}, [selectedSource, repoSearch, githubToken, githubProfile]);
// Find matching source when typing
useEffect(() => {
if (repoSearch.includes('/')) {
const match = sources.find(s =>
`${s.githubRepo.owner}/${s.githubRepo.repo}`.toLowerCase() === repoSearch.toLowerCase()
);
if (match) {
setSelectedSource(match);
} else {
setSelectedSource(null);
}
}
}, [repoSearch, sources]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// We must have a GitHub repository for Jules to use
if (!repoSearch) {
alert("Please enter a target GitHub repository (owner/repo).");
return;
}
// We must have a Hugging Face Space ID if import is enabled
if (isHfImportEnabled && !hfSpaceId) {
alert("Please enter a Hugging Face Space ID to import from.");
return;
}
const githubRepoId = selectedSource
? `${selectedSource.githubRepo.owner}/${selectedSource.githubRepo.repo}`
: repoSearch;
const config: NewSessionConfig = {
title,
prompt,
sourceId: selectedSource?.name || `sources/github/${githubRepoId}`, // Fallback ID if not found
githubRepoId,
branch,
hfImport: isHfImportEnabled ? { spaceId: hfSpaceId } : undefined,
runHfDeploymentCue
};
onSubmit(config);
};
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden flex flex-col max-h-[90vh]">
<div className="p-5 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<div className="flex items-center gap-2">
<div className="p-2 bg-indigo-100 text-indigo-600 rounded-lg">
<Play className="w-5 h-5" />
</div>
<h3 className="font-bold text-gray-800">Start New Session</h3>
</div>
{!isLoading && (
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-5 h-5" />
</button>
)}
</div>
<form onSubmit={handleSubmit} className="p-6 overflow-y-auto space-y-5">
{/* Session Details */}
<div className="space-y-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Session Title</label>
<input
required
type="text"
value={title}
onChange={(e) => {
setTitle(e.target.value);
setIsManualTitle(true);
}}
className="w-full px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
placeholder="e.g. Implement Login Feature"
disabled={isLoading}
/>
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Initial Prompt</label>
<textarea
required
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="w-full px-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none resize-none h-24 text-sm"
placeholder="What should Jules do?"
disabled={isLoading}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">GitHub Source (owner/repo)</label>
<div className="relative">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" />
<input
list="source-suggestions"
type="text"
value={repoSearch}
onChange={(e) => setRepoSearch(e.target.value)}
className={`w-full pl-9 pr-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none transition-colors ${
selectedSource ? 'border-green-300 bg-green-50' : 'border-gray-200'
}`}
placeholder="JsonLord/agent-notes"
disabled={isLoading}
/>
<datalist id="source-suggestions">
{sources.map(s => (
<option key={s.name} value={`${s.githubRepo.owner}/${s.githubRepo.repo}`} />
))}
</datalist>
</div>
{repoSearch && !selectedSource && availableBranches.length > 0 && (
<div className="flex items-center gap-1 mt-1 text-[10px] text-blue-600 font-bold uppercase">
<GitBranch className="w-3 h-3" />
<span>Connected via GitHub</span>
</div>
)}
{selectedSource && (
<div className="flex items-center gap-1 mt-1 text-[10px] text-green-600 font-bold uppercase">
<Github className="w-3 h-3" />
<span>Jules Source Verified</span>
</div>
)}
</div>
<div>
<label className="block text-sm font-semibold text-gray-700 mb-1">Branch</label>
<div className="relative">
<GitBranch className="absolute left-3 top-2.5 w-4 h-4 text-gray-400" />
<select
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="w-full pl-9 pr-8 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none bg-white appearance-none text-sm"
disabled={isLoading || isLoadingBranches}
>
{isLoadingBranches ? (
<option>Loading branches...</option>
) : availableBranches.length > 0 ? (
availableBranches.map(b => <option key={b} value={b}>{b}</option>)
) : (
<option value="main">main</option>
)}
</select>
{isLoadingBranches && <Loader2 className="absolute right-3 top-2.5 w-4 h-4 text-indigo-500 animate-spin" />}
</div>
</div>
</div>
</div>
{/* Hugging Face Import Section */}
<div className={`border rounded-xl transition-all overflow-hidden ${isHfImportEnabled ? 'border-indigo-200 bg-indigo-50/50' : 'border-gray-200'}`}>
<button
type="button"
onClick={() => setIsHfImportEnabled(!isHfImportEnabled)}
className="w-full flex items-center justify-between p-4 text-left"
disabled={isLoading}
>
<div className="flex items-center gap-2">
<Upload className={`w-4 h-4 ${isHfImportEnabled ? 'text-indigo-600' : 'text-gray-500'}`} />
<span className={`text-sm font-medium ${isHfImportEnabled ? 'text-indigo-900' : 'text-gray-700'}`}>
Import from Hugging Face Space
</span>
</div>
<div className={`w-4 h-4 rounded-full border flex items-center justify-center transition-colors ${isHfImportEnabled ? 'bg-indigo-600 border-indigo-600' : 'border-gray-300'}`}>
{isHfImportEnabled && <div className="w-1.5 h-1.5 bg-white rounded-full" />}
</div>
</button>
{isHfImportEnabled && (
<div className="p-4 pt-0 space-y-3 animate-in slide-in-from-top-2">
<p className="text-xs text-indigo-700">
The space will be uploaded to the selected GitHub repo/branch before the session starts.
</p>
<div>
<label className="block text-xs font-semibold text-indigo-800 mb-1">Hugging Face Space ID</label>
<input
type="text"
value={hfSpaceId}
onChange={(e) => setHfSpaceId(e.target.value)}
className="w-full px-3 py-2 border border-indigo-200 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none bg-white text-sm"
placeholder="e.g. huggingface/transformers-demo"
required={isHfImportEnabled}
disabled={isLoading}
/>
</div>
</div>
)}
</div>
<div className="pt-2">
<button
type="submit"
disabled={isLoading || (!repoSearch && !isHfImportEnabled)}
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-3 rounded-xl flex items-center justify-center gap-2 transition-transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{isHfImportEnabled ? 'Importing & Creating...' : 'Creating Session...'}
</>
) : (
<>
Create Session
<Play className="w-4 h-4 fill-current" />
</>
)}
</button>
{!repoSearch && !isHfImportEnabled && (
<p className="text-xs text-indigo-500 text-center mt-2">Enter a GitHub repo to start.</p>
)}
</div>
</form>
</div>
</div>
);
};