All files / src/components/Sidebar LeftSidebar.jsx

11.11% Statements 6/54
16.42% Branches 23/140
8.33% Functions 2/24
17.64% Lines 6/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219                          2x                             2x     2x   2x                               2x                                                                     2x                                                                                                                                                                                                                                                                            
import React, { useRef, useState } from 'react';
import { useLabState } from '../../useLabState';
import { api } from '../../api';
import { vscodeNative } from '../../vscodeNative';
import MarkdownText from '../MarkdownText';
import {
  FlaskConical, FolderOpen, Brain, Package, GraduationCap,
  Loader2, Settings, X, RefreshCw, CheckCircle, ShieldAlert,
  Globe, Search, StopCircle, Map, Activity, Clock, MoveRight,
  Archive, Trash2, Target
} from 'lucide-react';
 
export default function LeftSidebar() {
  const scrollRef = useRef(null);
  const {
    isSidebarOpen, setIsSidebarOpen,
    workspaceFiles, openPapersFolder,
    setShowKnowledge,
    researchRoadmap, setResearchRoadmap,
    researchMission, setResearchMission,
    isGeneratingPaper, generatePaper,
    journalEntries, messages, setMessages,
    activeWorkspace, showWorkspacePickerNative,
    backgroundTasks, hasActiveBackgroundTasks, models,
    deepResearchStatus, deepResearchElapsed, cancelDeepResearch,
    isLoading, isAutoRunning, isWaitingForTelemetry, agentLog, timeLeft, forceNextStep,
    selectedArtifact, setSelectedArtifact,
    setCycleCount,
    currentObjective, setCurrentObjective
  } = useLabState();
 
  const [expandedUserMsgs, setExpandedUserMsgs] = useState({});
 
  const confirmAndDelete = async (msg) => {
    const result = await vscodeNative.confirm('Delete this message?', 'Delete', 'Cancel');
    if (result === 'Delete') {
      await api.deleteMessage(msg.id);
      setMessages(prev => prev.filter(m => m.id !== msg.id));
      if (selectedArtifact?.id === msg.id) {
        setSelectedArtifact(null);
        setResearchRoadmap([]);
        setResearchMission('');
        setCycleCount(0);
        setCurrentObjective({ task: '', wait: 0 });
        api.updateWorkspace({ researchMission: '', researchRoadmap: [], cycleCount: 0 }).catch(err => console.warn('Sync:', err.message));
      }
    }
  };
 
  return (
    <>
      <div className={`fixed inset-0 z-[100] md:relative md:inset-auto md:z-auto md:flex-none md:w-80 md:border-r border-lab-border bg-lab-bg-secondary/95 md:bg-lab-bg-secondary/50 flex flex-col transition-all duration-300 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}`}>
        <div className="flex flex-col p-5 border-b border-lab-border bg-lab-bg-secondary/80">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2"><FlaskConical className="w-5 h-5 text-lab-accent" /><h1 className="font-bold text-lg tracking-tight">Science Lab</h1></div>
            <div className="flex gap-1">
              <button onClick={openPapersFolder} className={`p-2 rounded-lg transition-all hover:scale-110 ${workspaceFiles.length > 0 ? 'text-lab-accent bg-lab-accent/10' : 'text-lab-fg-secondary hover:bg-lab-bg-elevated'}`} title="Research Papers"><FolderOpen className="w-4 h-4" /></button>
              <button onClick={() => setShowKnowledge(true)} className="p-2 hover:bg-lab-bg-elevated rounded-lg text-lab-accent transition-all" title="Archive"><Brain className="w-4 h-4" /></button>
              <button onClick={async () => { const blob = await api.exportSession(researchRoadmap, researchMission); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `research-session-${Date.now()}.zip`; a.click(); URL.revokeObjectURL(url); }} className="p-2 hover:bg-lab-bg-elevated rounded-lg text-lab-fg-secondary transition-all" title="Export Session"><Package className="w-4 h-4" /></button>
              <button onClick={generatePaper} disabled={isGeneratingPaper || (!journalEntries.length && !messages.length)} className={`p-2 rounded-lg transition-all ${isGeneratingPaper ? 'text-lab-warning animate-pulse' : journalEntries.length || messages.length ? 'text-purple-400 hover:bg-purple-500/10' : 'text-lab-fg-muted cursor-not-allowed'}`} title="Generate Academic Paper">{isGeneratingPaper ? <Loader2 className="w-4 h-4 animate-spin" /> : <GraduationCap className="w-4 h-4" />}</button>
              <button onClick={() => vscodeNative.executeCommand('sciencelab.configureApiKeys')} className="p-2 hover:bg-lab-bg-elevated rounded-lg text-lab-fg-secondary transition-all" title="API Keys"><Settings className="w-4 h-4" /></button>
              <button onClick={() => setIsSidebarOpen(false)} className="md:hidden p-2 text-lab-fg-secondary"><X className="w-4 h-4" /></button>
            </div>
          </div>
          {activeWorkspace && (
            <button onClick={showWorkspacePickerNative} className="mt-2 flex items-center gap-2 text-[10px] text-lab-fg-secondary hover:text-lab-fg transition-colors group">
              <FolderOpen className="w-3 h-3" />
              <span className="truncate font-medium">{activeWorkspace.name}</span>
              <span className="text-lab-fg-muted group-hover:text-lab-fg-secondary ml-auto">Switch</span>
            </button>
          )}
        </div>
 
        {researchRoadmap.length > 0 && (
          <div className="bg-lab-bg/40 border-b border-lab-border p-4 space-y-3 shrink-0">
             <div className="flex items-center gap-2 mb-1">
              <Map className="w-3.5 h-3.5 text-lab-accent" />
              <span className="text-[10px] font-black uppercase tracking-widest text-lab-fg-secondary">Roadmap Phase</span>
            </div>
            <div className="text-[11px] font-medium text-lab-fg line-clamp-2 leading-snug border-l-2 border-lab-accent/30 pl-2 italic">
              <MarkdownText content={researchMission ? `"${researchMission}"` : '"No mission set"'} />
            </div>
            <div className="space-y-1.5 mt-4">
              {researchRoadmap.map((s, i) => (
                <div key={i} className={`flex items-center gap-2.5 text-[10px] px-2 py-1 rounded-md transition-colors ${s.status === 'active' ? 'bg-lab-accent/10 text-lab-accent font-bold' : s.status === 'complete' ? 'text-lab-fg-secondary' : 'text-lab-fg-muted'}`}>
                  {s.status === 'complete' ? <CheckCircle className="w-3 h-3 text-lab-accent" /> : s.status === 'active' ? <RefreshCw className="w-3 h-3 animate-spin" /> : <div className="w-3 h-3 rounded-full border border-lab-border" />}
                  <span className={s.status === 'complete' ? 'line-through opacity-50' : ''}>{String(s.label)}</span>
                </div>
              ))}
            </div>
          </div>
        )}
 
        <div className="flex-1 overflow-y-auto p-4 space-y-4" ref={scrollRef}>
          {backgroundTasks.length > 0 && (
            <div className="bg-lab-warning/5 border border-amber-500/20 rounded-2xl p-4 space-y-3 shadow-inner">
              <div className="flex items-center justify-between mb-1">
                <div className="flex items-center gap-2">
                  {hasActiveBackgroundTasks ? <Loader2 className="w-3.5 h-3.5 text-lab-warning animate-spin" /> : <CheckCircle className="w-3.5 h-3.5 text-lab-accent" />}
                  <span className="text-[10px] font-black uppercase tracking-widest text-lab-warning">
                    Background Research
                  </span>
                </div>
                <span className="text-[10px] font-mono text-lab-fg-secondary">
                  {backgroundTasks.filter(t => t.status === 'completed').length}/{backgroundTasks.length}
                </span>
              </div>
              {backgroundTasks.map(task => (
                <div key={task.id} className="flex items-start gap-2 text-[10px] pl-2 border-l-2 border-amber-500/20">
                  {task.status === 'running'
                    ? <RefreshCw className="w-3 h-3 text-lab-warning animate-spin mt-0.5 shrink-0" />
                    : task.status === 'completed'
                    ? <CheckCircle className="w-3 h-3 text-lab-accent mt-0.5 shrink-0" />
                    : <ShieldAlert className="w-3 h-3 text-lab-error mt-0.5 shrink-0" />}
                  <div className="min-w-0">
                    <p className="text-lab-fg-secondary font-medium truncate">{task.query.length > 80 ? task.query.substring(0, 80) + '...' : task.query}</p>
                    <span className={`inline-block mt-0.5 text-[8px] font-bold uppercase px-1.5 py-0.5 rounded ${
                      task.model === 'pro' ? 'bg-cyan-500/10 text-cyan-400' :
                      task.model.startsWith('deep') ? 'bg-purple-500/10 text-purple-400' :
                      'bg-lab-accent/10 text-lab-accent'
                    }`}>{models.find(m => m.id === task.model)?.name || task.model}</span>
                  </div>
                </div>
              ))}
            </div>
          )}
 
          {deepResearchStatus && (
            <div className={`border rounded-2xl p-4 space-y-3 shadow-inner ${deepResearchStatus.status === 'completed' ? 'bg-lab-accent/5 border-lab-accent/30' : deepResearchStatus.status === 'failed' ? 'bg-lab-error/5 border-red-500/30' : 'bg-indigo-500/5 border-indigo-500/30'}`}>
              <div className="flex items-center justify-between mb-1">
                <div className="flex items-center gap-2">
                  {deepResearchStatus.status === 'completed' ? <CheckCircle className="w-3.5 h-3.5 text-lab-accent" /> : deepResearchStatus.status === 'failed' ? <ShieldAlert className="w-3.5 h-3.5 text-lab-error" /> : <Globe className="w-3.5 h-3.5 text-indigo-400 animate-spin" />}
                  <span className={`text-[10px] font-black uppercase tracking-widest ${deepResearchStatus.status === 'completed' ? 'text-lab-accent' : deepResearchStatus.status === 'failed' ? 'text-lab-error' : 'text-indigo-400'}`}>
                    {deepResearchStatus.status === 'completed' ? 'Research Complete' : deepResearchStatus.status === 'failed' ? 'Research Failed' : 'Deep Research'}
                  </span>
                </div>
                <span className="text-[10px] font-mono text-lab-fg-secondary">{Math.floor(deepResearchElapsed / 60)}:{String(deepResearchElapsed % 60).padStart(2, '0')}</span>
              </div>
              <div className="flex items-center gap-2 text-[10px] text-lab-fg-secondary">
                <span className={`px-2 py-0.5 rounded-md text-[9px] font-bold uppercase ${(deepResearchStatus.model || '').includes('max') ? 'bg-purple-500/10 text-purple-400 border border-purple-500/20' : 'bg-blue-500/10 text-lab-accent border border-blue-500/20'}`}>
                  {models.find(m => m.id === deepResearchStatus.model)?.name || 'Deep Research'}
                </span>
              </div>
              {deepResearchStatus.status !== 'completed' && deepResearchStatus.status !== 'failed' && (
                <div className="space-y-2 pt-2 border-t border-indigo-500/10">
                  <div className="flex items-center gap-2 text-[9px] text-indigo-400/70">
                    <Search className="w-3 h-3 animate-pulse" />
                    <span>Searching, analyzing, and synthesizing...</span>
                  </div>
                  <button onClick={cancelDeepResearch} className="w-full py-2 bg-lab-error/10 hover:bg-lab-error/20 border border-red-500/20 rounded-lg text-[9px] font-bold text-lab-error flex items-center justify-center gap-1.5 transition-all">
                    <StopCircle className="w-3 h-3" /> CANCEL RESEARCH
                  </button>
                </div>
              )}
              {deepResearchStatus.status === 'completed' && (
                <p className="text-[9px] text-lab-accent/70 italic">Research complete — generating simulation...</p>
              )}
            </div>
          )}
 
 
          {messages.filter(m => !m.archived).map((msg) => {
            const isUser = msg.role === 'user';
            const isExpanded = expandedUserMsgs[msg.id];
            const collapsed = isUser && !isExpanded;
            return (
            <div key={msg.id} onClick={() => { if (isUser) { setExpandedUserMsgs(prev => ({ ...prev, [msg.id]: !prev[msg.id] })); } else { setSelectedArtifact(msg); setIsSidebarOpen(false); } }} className={`group relative rounded-2xl cursor-pointer transition-all border ${collapsed ? 'px-4 py-2' : 'p-4'} ${isUser ? 'bg-lab-bg-elevated/20 border-lab-border' : selectedArtifact?.id === msg.id ? 'bg-lab-button-bg/10 border-lab-accent ring-1 ring-lab-accent shadow-xl' : 'bg-lab-bg-elevated/60 border-lab-border hover:border-lab-border'}`}>
              <div className={`flex justify-between items-start ${collapsed ? '' : 'mb-1'}`}>
                <span className={`text-[9px] font-bold uppercase tracking-widest ${msg.synthetic ? 'text-purple-400' : msg.mode === 'ENHANCE' ? 'text-lab-accent' : 'text-lab-fg-secondary'}`}>{isUser ? (msg.synthetic ? 'AI User' : 'User') : msg.mode === 'FIX' ? 'Repair' : 'AI Researcher'}</span>
                {collapsed && <p className="text-[10px] text-lab-fg-muted truncate ml-3 flex-1">{String(msg.content)}</p>}
                <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
                  {!isUser && (
                    <button onClick={(e) => { e.stopPropagation(); api.patchMessage(msg.id, { archived: true }).then(() => { setMessages(prev => prev.map(m => m.id === msg.id ? { ...m, archived: true } : m)); if (selectedArtifact?.id === msg.id) setSelectedArtifact(null); }); }} className="p-1 hover:bg-lab-warning/20 rounded text-lab-fg-muted hover:text-lab-warning" title="Archive"><Archive className="w-3.5 h-3.5" /></button>
                  )}
                  <button onClick={(e) => { e.stopPropagation(); confirmAndDelete(msg); }} className="p-1 hover:bg-lab-error/20 rounded text-lab-fg-muted hover:text-lab-error"><Trash2 className="w-3.5 h-3.5" /></button>
                </div>
              </div>
              {!collapsed && <p className="text-xs font-medium text-lab-fg line-clamp-2 leading-tight">{isUser ? String(msg.content) : String(msg.prompt)}</p>}
              {!collapsed && msg.objective && <div className="mt-2 flex items-center gap-1.5 text-[8px] text-lab-warning/80 font-bold uppercase tracking-tight"><Target className="w-2.5 h-2.5" /> Next Phase: {String(msg.objective)}</div>}
            </div>
            );
          })}
          {(isLoading || isAutoRunning) && (
            <div className={`border rounded-2xl p-4 animate-pulse space-y-2 transition-all ${isWaitingForTelemetry ? 'bg-blue-500/10 border-blue-500/40 shadow-[0_0_15px_rgba(59,130,246,0.1)]' : isAutoRunning ? 'bg-lab-warning/10 border-amber-500/40' : 'bg-lab-accent/5 border-lab-accent/20'}`}>
              <div className="flex items-center justify-between mb-2">
                <div className="flex items-center gap-2">
                  <Activity className={`w-3 h-3 animate-pulse ${isWaitingForTelemetry ? 'text-lab-accent' : isAutoRunning ? 'text-lab-warning' : 'text-lab-accent'}`} />
                  <span className={`text-[10px] font-bold uppercase tracking-widest ${isWaitingForTelemetry ? 'text-lab-accent' : isAutoRunning ? 'text-lab-warning' : 'text-lab-accent'}`}>
                    {isWaitingForTelemetry ? 'Synthetic Researcher Observing' : isAutoRunning ? 'Synthetic User Thinking' : 'Neural Feed'}
                  </span>
                </div>
              </div>
              <div className="space-y-1.5">
                {agentLog.slice(-5).map((log, i) => (
                  <div key={i} className="flex gap-2 text-[10px] items-start border-l border-white/5 pl-2">
                    <RefreshCw className={`w-2.5 h-2.5 mt-0.5 animate-spin ${log.action.includes('Telemetry') ? 'text-lab-accent' : 'text-lab-fg-muted'}`} />
                    <div><span className="text-lab-fg-secondary font-bold uppercase">{String(log.action)}:</span> <span className="text-lab-fg-secondary leading-tight">{String(log.details)}</span></div>
                  </div>
                ))}
              </div>
              {isWaitingForTelemetry && (
                <div className="space-y-2 pt-1 border-t border-blue-500/10 mt-2">
                  <div className="flex justify-between items-center text-[8px] text-lab-accent/60 uppercase font-bold px-1">
                    <div className="flex items-center gap-1"><Clock className="w-2.5 h-2.5" /> Gathering Telemetry</div>
                    <span>{timeLeft}s remaining</span>
                  </div>
                  <button onClick={forceNextStep} className="w-full py-2 bg-blue-600/20 hover:bg-blue-600/40 border border-blue-500/30 rounded-lg text-[9px] font-bold text-lab-accent flex items-center justify-center gap-1.5 transition-all">
                    <MoveRight className="w-3 h-3" /> ASK NOW
                  </button>
                </div>
              )}
            </div>
          )}
        </div>
      </div>
    </>
  );
}