Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"test:ai-assist": "node --test tests/ai-assist-sample-inputs.test.mjs",
"preview": "vite preview"
},
"dependencies": {
Expand Down Expand Up @@ -56,7 +57,7 @@
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^7.7.1",
"recharts": "^2.12.7",
"recharts": "^2.15.4",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
Expand Down
138 changes: 53 additions & 85 deletions src/components/AIChallengeChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,23 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Send, Sparkles } from 'lucide-react';
import { supabase } from '@/lib/supabaseClient';
import { chatWithAI, generateChallengeFromChat, type AIGeneratedChallenge, type AIMessage } from '@/services/aiService';
import { useToast } from '@/hooks/use-toast';
import { getErrorMessage } from '@/lib/errorHandling';

interface Message {
role: 'user' | 'assistant';
content: string;
}

interface AIChallengeData {
title: string;
challengeId?: string;
associatedPathway?: string;
associatedModule?: string;
difficulty: 'Beginner' | 'Intermediate' | 'Advanced';
estimatedTime: number; // in minutes
challengeType: 'Build' | 'Modify' | 'Analyse' | 'Deploy' | 'Reflect';
recommendedTools: string[];
xp: string; // Always "(calculated by system)"
coverImageDescription: string;
versionNumber: string;
fullDescription: string;
requirements: string[];
}
const INITIAL_MESSAGE: AIMessage = {
role: 'assistant',
content: "Hi! I'm here to help you create a great challenge for NoCodeJam. What kind of challenge would you like to create? Tell me about your idea!"
};

interface AIChallengeChat {
open: boolean;
onOpenChange: (open: boolean) => void;
onChallengeGenerated: (data: AIChallengeData) => void;
onChallengeGenerated: (data: AIGeneratedChallenge) => void;
}

export function AIChallengeChat({ open, onOpenChange, onChallengeGenerated }: AIChallengeChat) {
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content: "Hi! I'm here to help you create a great challenge for NoCodeJam. What kind of challenge would you like to create? Tell me about your idea!"
}
]);
const [messages, setMessages] = useState<AIMessage[]>([INITIAL_MESSAGE]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
Expand All @@ -65,30 +45,24 @@ export function AIChallengeChat({ open, onOpenChange, onChallengeGenerated }: AI
setIsLoading(true);

// Add user message
const newMessages: Message[] = [...messages, { role: 'user', content: userMessage }];
const newMessages: AIMessage[] = [...messages, { role: 'user', content: userMessage }];
setMessages(newMessages);

try {
// Call Edge Function for chat
const { data, error } = await supabase.functions.invoke('generate-challenge', {
body: {
action: 'chat',
messages: newMessages
}
});

if (error) throw error;
if (data?.error) throw new Error(data.error);
const { message, fallback } = await chatWithAI(newMessages);
setMessages([...newMessages, { role: 'assistant', content: message }]);

// Add assistant response
if (data?.message) {
setMessages([...newMessages, { role: 'assistant', content: data.message }]);
if (fallback.fallbackUsed) {
toast({
title: "Fallback Response",
description: fallback.fallbackReason ?? "The AI service was unavailable, so a fallback response was used.",
});
}
} catch (err) {
console.error('Chat error:', err);
toast({
title: "Chat Error",
description: err instanceof Error ? err.message : "Failed to get response",
description: getErrorMessage(err),
variant: "destructive"
});
} finally {
Expand All @@ -97,61 +71,55 @@ export function AIChallengeChat({ open, onOpenChange, onChallengeGenerated }: AI
};

const generateChallenge = async () => {
if (messages.length < 2) {
toast({
title: "More Input Needed",
description: "Add at least one idea before generating a challenge.",
});
return;
}

setIsGenerating(true);

try {
// Call Edge Function to extract structured data from conversation
const { data, error } = await supabase.functions.invoke('generate-challenge', {
body: {
action: 'generate',
messages: messages
}
});
const { challenge, warnings, fallback } = await generateChallengeFromChat(messages);

if (error) throw error;
if (data?.error) throw new Error(data.error);

if (data?.challenge) {
// Validation Warnings
if (data.validationWarnings && data.validationWarnings.length > 0) {
toast({
title: "Governance Warnings",
description: (
<ul className="list-disc pl-4">
{data.validationWarnings.map((w: string, i: number) => (
<li key={i}>{w}</li>
))}
</ul>
),
variant: "default",
className: "border-yellow-500 border-l-4 bg-gray-800 text-white"
});
}

// Pass the structured challenge data back to parent
onChallengeGenerated(data.challenge);
if (warnings.length > 0) {
toast({
title: "Governance Warnings",
description: (
<ul className="list-disc pl-4">
{warnings.map((warning, index) => (
<li key={index}>{warning}</li>
))}
</ul>
),
variant: "default",
className: "border-yellow-500 border-l-4 bg-gray-800 text-white"
});
}

if (fallback.fallbackUsed) {
toast({
title: "Challenge Generated!",
description: "The form has been populated with your challenge details.",
title: "Fallback Draft Used",
description: fallback.fallbackReason ?? "A fallback challenge draft was created because the AI service was unavailable.",
});
}

// Close the modal
onOpenChange(false);
onChallengeGenerated(challenge);

// Reset for next time
setMessages([
{
role: 'assistant',
content: "Hi! I'm here to help you create a great challenge for NoCodeJam. What kind of challenge would you like to create? Tell me about your idea!"
}
]);
}
toast({
title: "Challenge Generated!",
description: "The form has been populated with your challenge details.",
});

onOpenChange(false);
setMessages([INITIAL_MESSAGE]);
} catch (err) {
console.error('Generation error:', err);
toast({
title: "Generation Error",
description: err instanceof Error ? err.message : "Failed to generate challenge",
description: getErrorMessage(err),
variant: "destructive"
});
} finally {
Expand Down
94 changes: 41 additions & 53 deletions src/components/AILearnChat.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
import React, { useState, useRef, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Send, BookOpen } from 'lucide-react';
import { supabase } from '@/lib/supabaseClient';
import { useToast } from '@/hooks/use-toast';

interface Message {
role: 'user' | 'assistant';
content: string;
}

interface AILearnChatProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function AILearnChat({ open, onOpenChange }: AILearnChatProps) {
const [messages, setMessages] = useState<Message[]>([
{
role: 'assistant',
content: "Hello! I'm your Learning Guide. What skills or tools would you like to learn today? Tell me your goals, and I'll recommend a pathway for you."
}
]);
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Send, BookOpen } from 'lucide-react';
import { chatWithLearningArchitect, type AIMessage } from '@/services/aiService';
import { useToast } from '@/hooks/use-toast';
import { getErrorMessage } from '@/lib/errorHandling';

const INITIAL_MESSAGE: AIMessage = {
role: 'assistant',
content: "Hello! I'm your Learning Guide. What skills or tools would you like to learn today? Tell me your goals, and I'll recommend a pathway for you."
};

interface AILearnChatProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function AILearnChat({ open, onOpenChange }: AILearnChatProps) {
const [messages, setMessages] = useState<AIMessage[]>([INITIAL_MESSAGE]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showPrompts, setShowPrompts] = useState(true);
Expand All @@ -47,34 +43,26 @@ export function AILearnChat({ open, onOpenChange }: AILearnChatProps) {
setIsLoading(true);

// UI update: Add user message immediately
const newMessages: Message[] = [...messages, { role: 'user', content: userMessage }];
setMessages(newMessages);

try {
const { data, error } = await supabase.functions.invoke('generate-challenge', {
body: {
action: 'chat-learn',
messages: newMessages
}
});

if (error) throw error;
if (data?.error) throw new Error(data.error);

// Add assistant response
if (data?.message) {
setMessages([...newMessages, { role: 'assistant', content: data.message }]);
} else {
throw new Error("No response message received");
}

} catch (err) {
console.error('Chat error:', err);
toast({
title: "Chat Error",
description: err instanceof Error ? err.message : "Failed to get response",
variant: "destructive"
});
const newMessages: AIMessage[] = [...messages, { role: 'user', content: userMessage }];
setMessages(newMessages);

try {
const { message, fallback } = await chatWithLearningArchitect(newMessages);
setMessages([...newMessages, { role: 'assistant', content: message }]);

if (fallback.fallbackUsed) {
toast({
title: "Fallback Response",
description: fallback.fallbackReason ?? "The AI service was unavailable, so a fallback learning response was used.",
});
}
} catch (err) {
console.error('Chat error:', err);
toast({
title: "Chat Error",
description: getErrorMessage(err),
variant: "destructive"
});
} finally {
setIsLoading(false);
}
Expand Down
Loading