From c6ff697b24531127599531c1fd8c8ff851356106 Mon Sep 17 00:00:00 2001 From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com> Date: Sat, 13 Sep 2025 09:24:06 -0400 Subject: [PATCH 01/14] Created /challenges route --- .../challenge/BaseChallengeWorkbench.tsx | 21 ++ .../challenge/ChallengeWorkbench.client.tsx | 211 ++++++++++++++++++ app/lib/challenges.ts | 28 +++ app/routes/challenges.$id.tsx | 75 +++++++ data/challenges.json | 12 + 5 files changed, 347 insertions(+) create mode 100644 app/components/challenge/BaseChallengeWorkbench.tsx create mode 100644 app/components/challenge/ChallengeWorkbench.client.tsx create mode 100644 app/lib/challenges.ts create mode 100644 app/routes/challenges.$id.tsx create mode 100644 data/challenges.json diff --git a/app/components/challenge/BaseChallengeWorkbench.tsx b/app/components/challenge/BaseChallengeWorkbench.tsx new file mode 100644 index 0000000000..d9831b6de3 --- /dev/null +++ b/app/components/challenge/BaseChallengeWorkbench.tsx @@ -0,0 +1,21 @@ +import { memo } from 'react'; + +export const BaseChallengeWorkbench = memo(() => { + return ( +
+ {/* Editor Panel Placeholder */} +
+
+
Loading editor...
+
+
+ + {/* Preview Panel Placeholder */} +
+
+
Loading preview...
+
+
+
+ ); +}); \ No newline at end of file diff --git a/app/components/challenge/ChallengeWorkbench.client.tsx b/app/components/challenge/ChallengeWorkbench.client.tsx new file mode 100644 index 0000000000..49718eb703 --- /dev/null +++ b/app/components/challenge/ChallengeWorkbench.client.tsx @@ -0,0 +1,211 @@ +import { useStore } from '@nanostores/react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; +import { toast } from 'react-toastify'; +import { + type OnChangeCallback as OnEditorChange, + type OnScrollCallback as OnEditorScroll, +} from '~/components/editor/codemirror/CodeMirrorEditor'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { renderLogger } from '~/utils/logger'; +import { EditorPanel } from '../workbench/EditorPanel'; +import { Preview } from '../workbench/Preview'; + +const CHALLENGE_SCAFFOLD = { + 'src/App.tsx': `import { useState } from 'react'; +import './App.css'; + +function App() { + return ( +
+

Challenge

+

Start building your solution here!

+
+ ); +} + +export default App;`, + + 'src/App.css': `.App { + text-align: center; + padding: 2rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Open Sans', 'Helvetica Neue', sans-serif; +} + +.App h1 { + color: #333; + margin-bottom: 1rem; +}`, + + 'src/main.tsx': `import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +);`, + + 'index.html': ` + + + + + + Challenge + + +
+ + +`, + + 'package.json': `{ + "name": "challenge", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.3", + "typescript": "^5.0.2", + "vite": "^4.4.5" + } +}`, + + 'vite.config.ts': `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +})`, + + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +}`, + + 'tsconfig.node.json': `{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +}` +}; + +export const ChallengeWorkbench = memo(() => { + renderLogger.trace('ChallengeWorkbench'); + + const currentDocument = useStore(workbenchStore.currentDocument); + const unsavedFiles = useStore(workbenchStore.unsavedFiles); + const files = useStore(workbenchStore.files); + const selectedFile = useStore(workbenchStore.selectedFile); + + // Initialize the challenge scaffold when component mounts + useEffect(() => { + const initializeScaffold = async () => { + try { + // Set up the basic scaffold files + const mockFiles = Object.entries(CHALLENGE_SCAFFOLD).reduce((acc, [path, content]) => { + acc[path] = { + type: 'file' as const, + content, + }; + return acc; + }, {} as any); + + workbenchStore.setDocuments(mockFiles); + workbenchStore.setShowWorkbench(true); + + // Select App.tsx by default + workbenchStore.setSelectedFile('src/App.tsx'); + } catch (error) { + console.error('Failed to initialize challenge scaffold:', error); + toast.error('Failed to initialize challenge environment'); + } + }; + + if (Object.keys(files).length === 0) { + initializeScaffold(); + } + }, [files]); + + const onEditorChange = useCallback((update) => { + workbenchStore.setCurrentDocumentContent(update.content); + }, []); + + const onEditorScroll = useCallback((position) => { + workbenchStore.setCurrentDocumentScrollPosition(position); + }, []); + + const onFileSelect = useCallback((filePath: string | undefined) => { + workbenchStore.setSelectedFile(filePath); + }, []); + + const onFileSave = useCallback(() => { + workbenchStore.saveCurrentDocument().catch(() => { + toast.error('Failed to update file content'); + }); + }, []); + + const onFileReset = useCallback(() => { + workbenchStore.resetCurrentDocument(); + }, []); + + return ( +
+ {/* Editor Panel */} +
+ +
+ + {/* Preview Panel */} +
+ +
+
+ ); +}); \ No newline at end of file diff --git a/app/lib/challenges.ts b/app/lib/challenges.ts new file mode 100644 index 0000000000..ffddf32bde --- /dev/null +++ b/app/lib/challenges.ts @@ -0,0 +1,28 @@ +export type Challenge = { + id: string; + title: string; + question: string; +}; + +// Inline challenge data to avoid JSON import issues +const challengesData: Challenge[] = [ + { + "id": "counter", + "title": "Build a Counter", + "question": "Make a click counter with +, -, and reset buttons. The value must never go below 0." + }, + { + "id": "todo", + "title": "Todo List", + "question": "Build a todo list with add and delete functionality." + } +]; + +export function loadChallenge(id: string): Challenge | null { + const challenge = challengesData.find((c) => c.id === id); + return challenge || null; +} + +export function getAllChallenges(): Challenge[] { + return challengesData; +} \ No newline at end of file diff --git a/app/routes/challenges.$id.tsx b/app/routes/challenges.$id.tsx new file mode 100644 index 0000000000..b97fca7a29 --- /dev/null +++ b/app/routes/challenges.$id.tsx @@ -0,0 +1,75 @@ +import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare'; +import { useLoaderData } from '@remix-run/react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { Header } from '~/components/header/Header'; +import { loadChallenge, type Challenge } from '~/lib/challenges'; +import { ChallengeWorkbench } from '~/components/challenge/ChallengeWorkbench.client'; +import { BaseChallengeWorkbench } from '~/components/challenge/BaseChallengeWorkbench'; + +export const meta: MetaFunction = ({ data }) => { + if (!data?.challenge) { + return [{ title: 'Challenge Not Found - Kleos Frontend' }]; + } + + return [ + { title: `${data.challenge.title} - Kleos Frontend` }, + { name: 'description', content: data.challenge.question } + ]; +}; + +export async function loader({ params }: LoaderFunctionArgs) { + const challengeId = params.id; + + if (!challengeId) { + throw new Response('Not Found', { status: 404 }); + } + + const challenge = loadChallenge(challengeId); + + if (!challenge) { + throw new Response('Not Found', { status: 404 }); + } + + return json({ challenge }); +} + +export default function ChallengePage() { + const { challenge } = useLoaderData(); + + return ( +
+
+
+ {/* Challenge Description Panel */} +
+
+
+

+ {challenge.title} +

+
+ Challenge ID: {challenge.id} +
+
+ +
+

+ Problem Statement +

+
+ {challenge.question} +
+
+
+
+ + {/* Editor and Preview Area */} +
+ }> + {() => } + +
+
+
+ ); +} \ No newline at end of file diff --git a/data/challenges.json b/data/challenges.json new file mode 100644 index 0000000000..99d82fc03d --- /dev/null +++ b/data/challenges.json @@ -0,0 +1,12 @@ +[ + { + "id": "counter", + "title": "Build a Counter", + "question": "Make a click counter with +, -, and reset buttons. The value must never go below 0." + }, + { + "id": "todo", + "title": "Todo List", + "question": "Build a todo list with add and delete functionality." + } +] \ No newline at end of file From b690a4224813f81d6fd398a5477794219bb5df87 Mon Sep 17 00:00:00 2001 From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:16:40 -0400 Subject: [PATCH 02/14] Challenge stuff is workn --- .../challenge/BaseChallengeChat.tsx | 186 ++++++++++++++ .../challenge/BaseChallengeWorkbench.tsx | 24 +- .../challenge/ChallengeWorkbench.client.tsx | 243 ++++++++++++++++-- app/routes/challenges.$id.tsx | 34 +-- 4 files changed, 425 insertions(+), 62 deletions(-) create mode 100644 app/components/challenge/BaseChallengeChat.tsx diff --git a/app/components/challenge/BaseChallengeChat.tsx b/app/components/challenge/BaseChallengeChat.tsx new file mode 100644 index 0000000000..386790cfd4 --- /dev/null +++ b/app/components/challenge/BaseChallengeChat.tsx @@ -0,0 +1,186 @@ +import type { Message } from 'ai'; +import React, { type RefCallback } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { Menu } from '~/components/sidebar/Menu.client'; +import { IconButton } from '~/components/ui/IconButton'; +import { classNames } from '~/utils/classNames'; +import { Messages } from '../chat/Messages.client'; +import { SendButton } from '../chat/SendButton.client'; +import type { Challenge } from '~/lib/challenges'; + +import styles from '../chat/BaseChat.module.scss'; + +interface BaseChallengeProps { + challenge: Challenge; + textareaRef?: React.RefObject | undefined; + messageRef?: RefCallback | undefined; + scrollRef?: RefCallback | undefined; + showChat?: boolean; + chatStarted?: boolean; + isStreaming?: boolean; + messages?: Message[]; + enhancingPrompt?: boolean; + promptEnhanced?: boolean; + input?: string; + handleStop?: () => void; + sendMessage?: (event: React.UIEvent, messageInput?: string) => void; + handleInputChange?: (event: React.ChangeEvent) => void; + enhancePrompt?: () => void; +} + +const TEXTAREA_MIN_HEIGHT = 76; + +export const BaseChallengeChat = React.forwardRef( + ( + { + challenge, + textareaRef, + messageRef, + scrollRef, + showChat = true, + chatStarted = false, + isStreaming = false, + enhancingPrompt = false, + promptEnhanced = false, + messages, + input = '', + sendMessage, + handleInputChange, + enhancePrompt, + handleStop, + }, + ref, + ) => { + const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; + + return ( +
+ {() => } +
+
+ {!chatStarted && ( +
+

+ {challenge.title} +

+

+ {challenge.question} +

+
+ )} +
+ + {() => { + return chatStarted ? ( + + ) : null; + }} + +
+
+