Spaces:
Paused
Paused
| 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> | |
| ); | |
| }; | |