Fix public access mode, introduce loading spinners, package fixes

This commit is contained in:
Alexander R.
2026-07-22 18:00:15 +00:00
parent 0b7259a011
commit b669d2e799
197 changed files with 698 additions and 20961 deletions
+36 -3
View File
@@ -1,8 +1,18 @@
import axios from 'axios';
const apiHost = typeof window !== 'undefined' && window.location.hostname ? window.location.hostname : '127.0.0.1';
const apiClient = axios.create({
baseURL: `http://${apiHost}:8000`,
const getApiBaseUrl = () => {
if (import.meta.env.VITE_API_BASE_URL) {
return import.meta.env.VITE_API_BASE_URL;
}
// Use relative path in browser so requests are routed via Vite proxy or reverse proxy
if (typeof window !== 'undefined') {
return '';
}
return 'http://127.0.0.1:8000';
};
export const apiClient = axios.create({
baseURL: getApiBaseUrl(),
headers: {
'Content-Type': 'application/json',
},
@@ -86,3 +96,26 @@ export const getHandbookExample = async (
});
return response.data;
};
export const generateChallenge = async (
prompt: string,
language: string,
difficulty: string,
subject: string
) => {
const response = await apiClient.post('/challenges/generate', {
prompt,
language,
difficulty,
subject,
});
return response.data;
};
export const saveChallenge = async (filename: string, markdown: string) => {
const response = await apiClient.post('/challenges/save', {
filename,
markdown,
});
return response.data;
};
+3 -34
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { getChallenges, Challenge } from '../api/client';
import { getChallenges, generateChallenge, saveChallenge, Challenge } from '../api/client';
import { MarkdownView } from '../components/MarkdownView';
import { BookOpen, ArrowRight, Code, ShieldCheck, Sparkles, X, Save, Layers } from 'lucide-react';
@@ -46,23 +46,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onSelectChallenge }) => {
setGenStatusMsg('⏳ Generating challenge via LLM backend...');
try {
const apiHost = typeof window !== 'undefined' && window.location.hostname ? window.location.hostname : '127.0.0.1';
const response = await fetch(`http://${apiHost}:8000/challenges/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: promptText,
language: genLanguage,
difficulty: genDifficulty,
subject: genSubject,
}),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const res = await response.json();
const res = await generateChallenge(promptText, genLanguage, genDifficulty, genSubject);
if (res.status === 'success') {
setGenMarkdown(res.markdown);
setGenFilename(res.filename);
@@ -87,22 +71,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onSelectChallenge }) => {
setGenStatusMsg('Saving challenge to disk...');
try {
const apiHost = typeof window !== 'undefined' && window.location.hostname ? window.location.hostname : '127.0.0.1';
const response = await fetch(`http://${apiHost}:8000/challenges/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: genFilename,
markdown: genMarkdown,
}),
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const res = await response.json();
const res = await saveChallenge(genFilename, genMarkdown);
if (res.status === 'success') {
setGenStatusMsg(`✓ Saved challenge to ${res.filename}`);
fetchChallengesList(); // Refresh dashboard list
+131 -48
View File
@@ -51,9 +51,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
const [rightWidth, setRightWidth] = useState(350); // Right Sidebar width in px
const [outputHeight, setOutputHeight] = useState(180); // Output Console height in px
const isDraggingLeft = useRef(false);
const isDraggingRight = useRef(false);
const isDraggingOutput = useRef(false);
const editorRef = useRef<any>(null);
const [draggingType, setDraggingType] = useState<'left' | 'right' | 'output' | null>(null);
const [output, setOutput] = useState<{ status: 'idle' | 'success' | 'error'; text: string }>({
status: 'idle',
@@ -102,11 +101,8 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
getHandbookCatalog(currentChallenge.language)
.then((catalog) => {
setHandbookCatalog(catalog);
if (catalog.functions.length > 0) {
setSelectedTopic(catalog.functions[0]);
} else if (catalog.subjects.length > 0) {
setSelectedTopic(catalog.subjects[0]);
}
setSelectedTopic(null);
setHandbookExample('');
})
.catch((err) => console.error('Failed to load handbook catalog:', err));
}
@@ -114,23 +110,27 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
// Global mousemove and mouseup listeners for drag-to-resize splitters
useEffect(() => {
if (!draggingType) return;
const handleMouseMove = (e: MouseEvent) => {
if (isDraggingLeft.current) {
const newWidth = Math.max(180, Math.min(600, e.clientX));
if (draggingType === 'left') {
const newWidth = Math.max(150, Math.min(Math.floor(window.innerWidth * 0.6), e.clientX));
setLeftWidth(newWidth);
} else if (isDraggingRight.current) {
const newWidth = Math.max(220, Math.min(700, window.innerWidth - e.clientX));
} else if (draggingType === 'right') {
const newWidth = Math.max(180, Math.min(Math.floor(window.innerWidth * 0.6), window.innerWidth - e.clientX));
setRightWidth(newWidth);
} else if (isDraggingOutput.current) {
const newHeight = Math.max(80, Math.min(650, window.innerHeight - e.clientY));
} else if (draggingType === 'output') {
const maxHeight = Math.max(150, window.innerHeight - 150);
const newHeight = Math.max(60, Math.min(maxHeight, window.innerHeight - e.clientY));
setOutputHeight(newHeight);
if (editorRef.current) {
try { editorRef.current.layout(); } catch (_) {}
}
}
};
const handleMouseUp = () => {
isDraggingLeft.current = false;
isDraggingRight.current = false;
isDraggingOutput.current = false;
setDraggingType(null);
document.body.style.cursor = 'default';
document.body.style.userSelect = 'auto';
};
@@ -141,25 +141,31 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
}, [draggingType]);
useEffect(() => {
if (editorRef.current) {
try { editorRef.current.layout(); } catch (_) {}
}
}, [outputHeight, leftWidth, rightWidth]);
const startDraggingLeft = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingLeft.current = true;
setDraggingType('left');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
};
const startDraggingRight = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingRight.current = true;
setDraggingType('right');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
};
const startDraggingOutput = (e: React.MouseEvent) => {
e.preventDefault();
isDraggingOutput.current = true;
setDraggingType('output');
document.body.style.cursor = 'row-resize';
document.body.style.userSelect = 'none';
};
@@ -225,7 +231,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
setIsLoadingGuidance(true);
setRightSidebarMode('mentor');
setGuidance('⏳ **Mentor is thinking and generating guidance...**\n\n*Analyzing your code, task requirements, and question...*');
setGuidance('');
setUserQuestion('');
try {
@@ -399,10 +405,39 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{/* Main Container */}
<div className="flex flex-1 overflow-hidden relative">
{/* Full-Screen Drag Overlay to bypass Monaco Editor event capture */}
{draggingType && (
<div
onMouseMove={(e) => {
if (draggingType === 'left') {
const newWidth = Math.max(150, Math.min(Math.floor(window.innerWidth * 0.6), e.clientX));
setLeftWidth(newWidth);
} else if (draggingType === 'right') {
const newWidth = Math.max(180, Math.min(Math.floor(window.innerWidth * 0.6), window.innerWidth - e.clientX));
setRightWidth(newWidth);
} else if (draggingType === 'output') {
const maxHeight = Math.max(150, window.innerHeight - 150);
const newHeight = Math.max(60, Math.min(maxHeight, window.innerHeight - e.clientY));
setOutputHeight(newHeight);
if (editorRef.current) {
try { editorRef.current.layout(); } catch (_) {}
}
}
}}
onMouseUp={() => {
setDraggingType(null);
document.body.style.cursor = 'default';
document.body.style.userSelect = 'auto';
}}
className={`fixed inset-0 z-[99999] select-none ${
draggingType === 'output' ? 'cursor-row-resize' : 'cursor-col-resize'
}`}
/>
)}
{/* Left Column: Challenge Specifications (Scalable Width) */}
<div
style={{ width: `${leftWidth}px` }}
className="border-r border-darkBorder bg-darkBg flex flex-col overflow-hidden shrink-0"
className="border-r border-transparent bg-darkBg flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 bg-darkSurface border-b border-darkBorder flex justify-between items-center">
<h2 className="text-xs font-bold uppercase tracking-wider text-gray-400">Challenge Details</h2>
@@ -475,21 +510,21 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{/* Resizable Splitter Handle: Left Column */}
<div
onMouseDown={startDraggingLeft}
className="w-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors"
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag to resize Challenge Panel"
>
<GripVertical size={10} className="text-gray-600 group-hover:text-white" />
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
{/* Middle Column: Monaco Code Editor & Resizable Output Console */}
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden">
<div className="flex-1 flex flex-col bg-[#1e1e1e] overflow-hidden min-w-0 min-h-0">
<div className="bg-darkBg text-gray-400 px-4 py-2 text-xs flex justify-between items-center border-b border-darkBorder shrink-0">
<span className="font-mono text-blue-400">main.py</span>
<span className="text-[11px] text-gray-500">TactiTerm IDE</span>
</div>
{/* Code Editor */}
<div className="flex-1 relative">
<div className="flex-1 min-h-0 relative">
<Editor
height="100%"
defaultLanguage={currentChallenge?.language.toLowerCase() || 'python'}
@@ -497,7 +532,10 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
defaultValue=""
value={code}
theme="vs-dark"
onMount={(_, monaco) => registerMonacoCompletions(monaco)}
onMount={(editor, monaco) => {
editorRef.current = editor;
registerMonacoCompletions(monaco);
}}
onChange={(val) => setCode(val || '')}
options={{
minimap: { enabled: false },
@@ -514,16 +552,16 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{/* Resizable Splitter Handle: Output Console */}
<div
onMouseDown={startDraggingOutput}
className="h-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-row-resize flex items-center justify-center shrink-0 group transition-colors"
className="h-2.5 -my-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-row-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag up/down to resize Output Console"
>
<GripHorizontal size={10} className="text-gray-600 group-hover:text-white" />
<GripHorizontal size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
{/* Scalable Output & Stdin Input Terminal Area */}
<div
style={{ height: `${outputHeight}px` }}
className="border-t border-darkBorder bg-black/90 p-4 font-mono text-xs flex flex-col overflow-hidden shrink-0"
className="border-t border-transparent bg-black/90 p-4 font-mono text-xs flex flex-col overflow-hidden shrink-0 min-h-0"
>
<div className="flex justify-between items-center text-gray-400 pb-2 mb-2 border-b border-gray-800 text-[11px] uppercase font-bold shrink-0">
<div className="flex items-center gap-2">
@@ -566,10 +604,10 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{rightSidebarMode !== 'closed' && (
<div
onMouseDown={startDraggingRight}
className="w-1.5 bg-darkBorder hover:bg-blue-500 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors"
className="w-2.5 -mx-1 z-10 bg-transparent hover:bg-blue-500/50 active:bg-blue-600 cursor-col-resize flex items-center justify-center shrink-0 group transition-colors select-none"
title="Drag to resize Right Sidebar"
>
<GripVertical size={10} className="text-gray-600 group-hover:text-white" />
<GripVertical size={12} className="text-gray-600/40 group-hover:text-white" />
</div>
)}
@@ -577,7 +615,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{rightSidebarMode === 'mentor' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-darkBorder bg-darkSurface flex flex-col overflow-hidden shrink-0"
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
<h2 className="flex items-center gap-2 font-bold text-amber-400 text-xs uppercase tracking-wider">
@@ -593,9 +631,18 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{/* Mentor Guidance View */}
<div className="flex-1 p-4 overflow-y-auto space-y-4">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
<MarkdownView content={guidance} />
</div>
{isLoadingGuidance ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<div className="relative flex items-center justify-center">
<div className="w-10 h-10 border-4 border-amber-500/20 border-t-amber-400 rounded-full animate-spin" />
<Sparkles size={16} className="absolute text-amber-400 animate-pulse" />
</div>
</div>
) : (
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
<MarkdownView content={guidance} />
</div>
)}
</div>
{/* Question Input Area */}
@@ -616,7 +663,11 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
disabled={isLoadingGuidance}
className="flex-1 py-2 bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1.5 shadow-sm"
>
<Send size={14} />
{isLoadingGuidance ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Send size={14} />
)}
<span>Ask Mentor</span>
</button>
@@ -626,7 +677,11 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
className="py-2 px-3 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-white font-semibold text-xs rounded-lg transition-all flex items-center justify-center gap-1"
title="Check Answer"
>
<Sparkles size={14} />
{isLoadingGuidance ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<Sparkles size={14} />
)}
<span>Check</span>
</button>
</div>
@@ -638,7 +693,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
{rightSidebarMode === 'handbook' && (
<div
style={{ width: `${rightWidth}px` }}
className="border-l border-darkBorder bg-darkSurface flex flex-col overflow-hidden shrink-0"
className="border-l border-transparent bg-darkSurface flex flex-col overflow-hidden shrink-0"
>
<div className="p-3 border-b border-darkBorder bg-darkBg flex justify-between items-center">
<h2 className="flex items-center gap-2 font-bold text-indigo-400 text-xs uppercase tracking-wider">
@@ -691,8 +746,25 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
</div>
</div>
{/* Topic Catalog List */}
<div className="p-3 border-b border-darkBorder bg-darkBg flex flex-col gap-1.5 max-h-48 overflow-y-auto">
{/* Topic Catalog List (Fills full height when no topic selected) */}
<div className={`p-3 bg-darkBg flex flex-col gap-1.5 overflow-y-auto ${
selectedTopic ? 'max-h-52 border-b border-darkBorder' : 'flex-1'
}`}>
{selectedTopic && (
<div className="flex justify-between items-center pb-1 text-[11px] text-gray-400 font-sans border-b border-darkBorder/40 mb-1">
<span>Selected: <strong className="text-indigo-400">{selectedTopic.title}</strong></span>
<button
onClick={() => {
setSelectedTopic(null);
setHandbookExample('');
}}
className="text-indigo-400 hover:text-indigo-300 underline text-[10px] font-semibold"
>
Clear selection (Show all)
</button>
</div>
)}
{filteredList.length === 0 ? (
<p className="text-xs text-gray-500 italic p-2">No matching functions or topics found.</p>
) : (
@@ -700,7 +772,7 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
<button
key={item.id}
onClick={() => handleSelectHandbookTopic(item)}
className={`text-left p-2 rounded-lg text-xs transition-all border ${
className={`text-left p-2.5 rounded-lg text-xs transition-all border ${
selectedTopic?.id === item.id
? 'bg-indigo-600/20 border-indigo-500/50 text-indigo-300 font-bold'
: 'bg-darkSurface border-darkBorder/50 text-gray-300 hover:bg-gray-800'
@@ -716,12 +788,23 @@ const Workspace: React.FC<WorkspaceProps> = ({ challengeId, onBack }) => {
)}
</div>
{/* Generated LLM Reference Card View */}
<div className="flex-1 p-4 overflow-y-auto">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
<MarkdownView content={handbookExample} />
{/* Generated LLM Reference Card View (ONLY renders after selecting a topic) */}
{selectedTopic && (
<div className="flex-1 p-4 overflow-y-auto bg-darkSurface border-t border-darkBorder">
<div className="p-4 bg-darkBg border border-darkBorder rounded-xl shadow-inner">
{isLoadingExample ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<div className="relative flex items-center justify-center">
<div className="w-10 h-10 border-4 border-indigo-500/20 border-t-indigo-400 rounded-full animate-spin" />
<BookMarked size={16} className="absolute text-indigo-400 animate-pulse" />
</div>
</div>
) : (
<MarkdownView content={handbookExample} />
)}
</div>
</div>
</div>
)}
</div>
)}
</div>