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 | 5x 5x 5x 35x 30x | import React from 'react';
import { useLabState } from '../../useLabState';
import {
Send, Trash2, X, Archive, Target, ImageIcon, Plus, Mic, MicOff, Loader2, RefreshCw
} from 'lucide-react';
import { api } from '../../api';
import { vscodeNative } from '../../vscodeNative';
import MarkdownText from '../MarkdownText';
export function ChatMessages() {
const {
messages, setMessages, selectedArtifact, setSelectedArtifact,
setResearchRoadmap, setResearchMission, setCycleCount,
setCurrentObjective, isSidebarOpen, setIsSidebarOpen,
handleRerun, isLoading
} = useLabState();
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 (
<>
{messages.filter(m => !m.archived).map((msg) => (
<div key={msg.id} onClick={() => { setSelectedArtifact(msg); setIsSidebarOpen(false); }} className={`group relative p-4 rounded-2xl cursor-pointer transition-all border ${msg.role === 'user' ? '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 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'}`}>{msg.role === 'user' ? (msg.synthetic ? 'AI User' : 'User') : msg.mode === 'FIX' ? 'Repair' : 'AI Researcher'}</span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{msg.role === 'assistant' && (
<>
<button onClick={(e) => { e.stopPropagation(); handleRerun(msg); }} disabled={isLoading} className="p-1 hover:bg-lab-accent/20 rounded text-lab-fg-muted hover:text-lab-accent disabled:opacity-30" title="Rerun"><RefreshCw className="w-3.5 h-3.5" /></button>
<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>
<div className="text-xs font-medium text-lab-fg line-clamp-2 leading-tight">
<MarkdownText content={msg.role === 'user' ? (msg.content || '') : (msg.prompt || '')} />
</div>
{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>
))}
</>
);
}
export function ChatInput() {
const {
input, setInput, setSelectedArtifact, isFullscreen, setIsSidebarOpen, setIsAutoRunning,
setResearchRoadmap, setResearchMission, setCycleCount, imageContext,
setImageContext, observerInputRef, isListening, setIsListening,
isLoading, hasActiveBackgroundTasks, handleSend, handlePaste,
mediaRecorderRef, audioChunksRef, sttIntervalRef
} = useLabState();
return (
<>
<div className={`shrink-0 border-t border-lab-border bg-lab-bg-secondary/60 backdrop-blur-md ${isFullscreen ? 'hidden' : ''}`}>
<div className="max-w-4xl mx-auto px-4 py-3">
<div className="flex gap-2 items-center">
<button onClick={() => { setSelectedArtifact(null); setInput(''); setIsSidebarOpen(true); setIsAutoRunning(false); setResearchRoadmap([]); setResearchMission(''); setCycleCount(0); api.updateWorkspace({ researchMission: '', researchRoadmap: [], cycleCount: 0 }).catch(err => console.warn('Sync:', err.message)); }} className="shrink-0 p-2.5 bg-lab-bg-elevated hover:bg-lab-bg-elevated text-lab-fg-secondary rounded-xl transition-all active:scale-95" title="New Session"><Plus className="w-5 h-5" /></button>
<button onClick={() => observerInputRef.current?.click()} className={`shrink-0 p-2.5 rounded-xl transition-all active:scale-95 ${imageContext ? 'bg-lab-button-bg text-white' : 'bg-lab-bg-elevated text-lab-fg-secondary hover:bg-lab-bg-elevated'}`} title="Attach Image"><ImageIcon className="w-5 h-5" /></button>
<div className="flex-1 relative">
{imageContext && (
<div className="absolute -top-12 left-0 flex items-center gap-2 px-2 py-1 bg-lab-bg-elevated border border-lab-border rounded-lg z-10">
<img src={imageContext} alt="attached" className="w-8 h-8 rounded object-cover" />
<span className="text-[10px] text-lab-fg-secondary">Image attached</span>
<button onClick={() => setImageContext(null)} className="p-0.5 hover:bg-lab-bg-elevated rounded text-lab-fg-secondary hover:text-lab-error"><X className="w-3 h-3" /></button>
</div>
)}
<textarea value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), handleSend())} onPaste={handlePaste} disabled={hasActiveBackgroundTasks} placeholder={hasActiveBackgroundTasks ? "Background research in progress..." : "Describe a research topic or concept..."} className={`w-full bg-lab-bg border border-lab-border/50 rounded-xl py-3 px-4 pr-10 focus:outline-none focus:ring-1 focus:ring-lab-accent/50 focus:border-lab-accent/50 text-sm h-[44px] resize-none overflow-hidden transition-all placeholder:text-lab-fg-muted ${hasActiveBackgroundTasks ? 'opacity-50 cursor-not-allowed' : ''}`} />
<button onClick={async () => {
if (isListening) {
mediaRecorderRef.current?.stop();
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/mp4';
const recorder = new MediaRecorder(stream, { mimeType });
mediaRecorderRef.current = recorder;
audioChunksRef.current = [];
setIsListening(true);
recorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunksRef.current.push(e.data);
};
const transcribeChunks = async () => {
if (audioChunksRef.current.length === 0) return;
const blob = new Blob(audioChunksRef.current, { type: mimeType });
const reader = new FileReader();
reader.onloadend = async () => {
const base64 = reader.result.split(',')[1];
if (!base64) return;
try {
const result = await api.stt(base64, mimeType.split(';')[0]);
if (result.text) setInput(result.text);
} catch {}
};
reader.readAsDataURL(blob);
};
sttIntervalRef.current = setInterval(transcribeChunks, 4000);
recorder.onstop = async () => {
clearInterval(sttIntervalRef.current);
stream.getTracks().forEach(t => t.stop());
setIsListening(false);
mediaRecorderRef.current = null;
if (audioChunksRef.current.length > 0) {
const blob = new Blob(audioChunksRef.current, { type: mimeType });
const reader = new FileReader();
reader.onloadend = async () => {
const base64 = reader.result.split(',')[1];
if (!base64) return;
try {
const result = await api.stt(base64, mimeType.split(';')[0]);
if (result.text) setInput(result.text);
} catch {}
};
reader.readAsDataURL(blob);
}
};
recorder.start(1000);
} catch (err) {
console.error('Mic access denied:', err);
setIsListening(false);
}
}} className={`absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-md transition-colors ${isListening ? 'text-lab-error animate-pulse bg-lab-error/10' : 'text-lab-fg-muted hover:text-lab-accent'}`}>{isListening ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}</button>
</div>
<button onClick={handleSend} disabled={isLoading || !input.trim() || hasActiveBackgroundTasks} className="shrink-0 p-2.5 bg-lab-button-bg hover:bg-lab-button-bg disabled:opacity-30 rounded-xl text-white active:scale-90 transition-all"><Send className="w-5 h-5" /></button>
</div>
</div>
</div>
</>
);
}
|