From 4d85fd4eb4c13783ce07acb71018b0784bf4e9bf Mon Sep 17 00:00:00 2001 From: vukrosic Date: Thu, 25 Sep 2025 14:30:34 +0200 Subject: [PATCH 01/30] Implement conversation management and message handling in Chatbot component - Refactored Chatbot component to manage conversations using Convex. - Added functionality to create and retrieve conversations and messages. - Integrated message saving for user, assistant, and system responses. - Enhanced user experience by loading previous conversation messages and maintaining context. - Updated schema to include conversations and messages tables for better data organization. --- components/chatbot.tsx | 166 +++++++++++++++++++++++++++++++---------- convex/chat.ts | 110 +++++++++++++++++++++++---- convex/schema.ts | 23 ++++++ 3 files changed, 243 insertions(+), 56 deletions(-) diff --git a/components/chatbot.tsx b/components/chatbot.tsx index 36b3164..0329f2f 100644 --- a/components/chatbot.tsx +++ b/components/chatbot.tsx @@ -1,8 +1,9 @@ 'use client'; import React, { useState, useRef, useEffect } from 'react'; -import { useAction } from 'convex/react'; +import { useAction, useMutation, useQuery } from 'convex/react'; import { api } from '../convex/_generated/api'; +import { Id } from '../convex/_generated/dataModel'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -31,7 +32,7 @@ interface ToolExecution { } interface ChatbotProps { - projectId: string; + projectId: Id<"projects">; projectName: string; } @@ -137,34 +138,23 @@ const mockTools = { // This will be replaced by the Convex action call export default function Chatbot({ projectId, projectName }: ChatbotProps) { - const [messages, setMessages] = useState([ - { - id: '1', - type: 'assistant', - content: `Hello! I'm your AI research assistant for the "${projectName}" project. - -I can help you with: -- **General questions** about your research and project -- **Running experiments** (say "run experiment" to use tools) -- **Data analysis** (say "analyze data" to use tools) -- **Model training** (say "train model" to use tools) -- **Model deployment** (say "deploy model" to use tools) -- **Google Colab notebooks** (say "create colab notebook" to generate notebooks) - -I'll only use tools when you explicitly ask me to run experiments or use MCP tools. Otherwise, I'll just chat and provide guidance. - -What would you like to work on today?`, - timestamp: new Date() - } - ]); + const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [copiedCodeBlocks, setCopiedCodeBlocks] = useState>(new Set()); const [copiedMessages, setCopiedMessages] = useState>(new Set()); + const [currentConversationId, setCurrentConversationId] = useState | null>(null); const scrollAreaRef = useRef(null); - // Use Convex action for AI chat + // Convex hooks const chatWithGrok = useAction(api.chat.chatWithGrok); + const createConversation = useMutation(api.chat.createConversation); + const addMessage = useMutation(api.chat.addMessage); + const conversations = useQuery(api.chat.getConversations, { projectId }); + const conversationMessages = useQuery( + api.chat.getMessages, + currentConversationId ? { conversationId: currentConversationId } : "skip" + ); const scrollToBottom = () => { if (scrollAreaRef.current) { @@ -176,6 +166,53 @@ What would you like to work on today?`, scrollToBottom(); }, [messages]); + // Load conversation messages when conversation changes + useEffect(() => { + if (conversationMessages) { + const formattedMessages: Message[] = conversationMessages.map(msg => ({ + id: msg._id, + type: msg.role as 'user' | 'assistant' | 'system', + content: msg.content, + timestamp: new Date(msg.timestamp), + tools: msg.tools + })); + setMessages(formattedMessages); + } + }, [conversationMessages]); + + // Create initial conversation if none exists + useEffect(() => { + if (!currentConversationId && conversations && conversations.length === 0) { + createConversation({ + projectId, + title: `Chat with ${projectName}` + }).then(conversationId => { + setCurrentConversationId(conversationId); + // Add welcome message + addMessage({ + conversationId, + role: 'assistant', + content: `Hello! I'm your AI research assistant for the "${projectName}" project. + +I can help you with: +- **General questions** about your research and project +- **Running experiments** (say "run experiment" to use tools) +- **Data analysis** (say "analyze data" to use tools) +- **Model training** (say "train model" to use tools) +- **Model deployment** (say "deploy model" to use tools) +- **Google Colab notebooks** (say "create colab notebook" to generate notebooks) + +I'll only use tools when you explicitly ask me to run experiments or use MCP tools. Otherwise, I'll just chat and provide guidance. + +What would you like to work on today?` + }); + }); + } else if (conversations && conversations.length > 0 && !currentConversationId) { + // Load the most recent conversation + setCurrentConversationId(conversations[0]._id); + } + }, [conversations, currentConversationId, projectId, projectName, createConversation, addMessage]); + const copyToClipboard = async (text: string, type: 'code' | 'message', id: string) => { try { await navigator.clipboard.writeText(text); @@ -204,7 +241,7 @@ What would you like to work on today?`, }; const handleSendMessage = async () => { - if (!inputMessage.trim() || isLoading) return; + if (!inputMessage.trim() || isLoading || !currentConversationId) return; const userMessage: Message = { id: Date.now().toString(), @@ -214,27 +251,43 @@ What would you like to work on today?`, }; setMessages(prev => [...prev, userMessage]); + + // Save user message to database + await addMessage({ + conversationId: currentConversationId, + role: 'user', + content: inputMessage + }); + + const messageToSend = inputMessage; setInputMessage(''); setIsLoading(true); try { // Check if user explicitly wants to run experiments or use tools - const shouldUseTools = inputMessage.toLowerCase().includes('run experiment') || - inputMessage.toLowerCase().includes('use mcp') || - inputMessage.toLowerCase().includes('run tool') || - inputMessage.toLowerCase().includes('execute') || - inputMessage.toLowerCase().includes('train model') || - inputMessage.toLowerCase().includes('analyze data') || - inputMessage.toLowerCase().includes('deploy model') || - inputMessage.toLowerCase().includes('create colab') || - inputMessage.toLowerCase().includes('colab notebook') || - inputMessage.toLowerCase().includes('generate notebook') || - inputMessage.toLowerCase().includes('open colab'); + const shouldUseTools = messageToSend.toLowerCase().includes('run experiment') || + messageToSend.toLowerCase().includes('use mcp') || + messageToSend.toLowerCase().includes('run tool') || + messageToSend.toLowerCase().includes('execute') || + messageToSend.toLowerCase().includes('train model') || + messageToSend.toLowerCase().includes('analyze data') || + messageToSend.toLowerCase().includes('deploy model') || + messageToSend.toLowerCase().includes('create colab') || + messageToSend.toLowerCase().includes('colab notebook') || + messageToSend.toLowerCase().includes('generate notebook') || + messageToSend.toLowerCase().includes('open colab'); + + // Prepare conversation history for the AI + const conversationHistory = messages.slice(0, -1).map(msg => ({ + role: msg.type as 'user' | 'assistant' | 'system', + content: msg.content + })); const response = await chatWithGrok({ - message: inputMessage, + message: messageToSend, context: projectName, - projectName: projectName + projectName: projectName, + conversationHistory }); const assistantMessage: Message = { @@ -251,6 +304,18 @@ What would you like to work on today?`, setMessages(prev => [...prev, assistantMessage]); + // Save assistant message to database + await addMessage({ + conversationId: currentConversationId, + role: 'assistant', + content: response.response, + tools: shouldUseTools && response.tools ? response.tools.map((toolName: string) => ({ + id: `${toolName}_${Date.now()}`, + name: mockTools[toolName as keyof typeof mockTools].name, + status: 'pending' as const + })) : [] + }); + // Execute tools if any and user explicitly requested them if (shouldUseTools && response.tools && response.tools.length > 0) { for (const toolName of response.tools) { @@ -288,16 +353,25 @@ What would you like to work on today?`, )); // Add result message + const resultContent = tool.name === 'Create Colab Notebook' && (result as any).colabUrl + ? `✅ ${tool.name} completed successfully!\n\n📓 **Notebook Created**: ${(result as any).title}\n🔗 **Colab Link**: [Open in Google Colab](${(result as any).colabUrl})\n\nClick the link above to open your notebook in Google Colab and start running your code!` + : `✅ ${tool.name} completed successfully!`; + const resultMessage: Message = { id: (Date.now() + 2).toString(), type: 'system', - content: tool.name === 'Create Colab Notebook' && (result as any).colabUrl - ? `✅ ${tool.name} completed successfully!\n\n📓 **Notebook Created**: ${(result as any).title}\n🔗 **Colab Link**: [Open in Google Colab](${(result as any).colabUrl})\n\nClick the link above to open your notebook in Google Colab and start running your code!` - : `✅ ${tool.name} completed successfully!`, + content: resultContent, timestamp: new Date() }; setMessages(prev => [...prev, resultMessage]); + // Save result message to database + await addMessage({ + conversationId: currentConversationId, + role: 'system', + content: resultContent + }); + } catch (error) { // Update tool status to failed setMessages(prev => prev.map(msg => @@ -318,13 +392,23 @@ What would you like to work on today?`, } } catch (error) { + const errorContent = "I'm sorry, I encountered an error. Please try again."; const errorMessage: Message = { id: (Date.now() + 1).toString(), type: 'assistant', - content: "I'm sorry, I encountered an error. Please try again.", + content: errorContent, timestamp: new Date() }; setMessages(prev => [...prev, errorMessage]); + + // Save error message to database + if (currentConversationId) { + await addMessage({ + conversationId: currentConversationId, + role: 'assistant', + content: errorContent + }); + } } finally { setIsLoading(false); } diff --git a/convex/chat.ts b/convex/chat.ts index 4197843..059c207 100644 --- a/convex/chat.ts +++ b/convex/chat.ts @@ -1,14 +1,89 @@ -import { action } from "./_generated/server"; +import { action, mutation, query } from "./_generated/server"; import { v } from "convex/values"; import OpenAI from "openai"; +// Create a new conversation +export const createConversation = mutation({ + args: { + projectId: v.id("projects"), + title: v.string(), + }, + handler: async (ctx, { projectId, title }) => { + const now = Date.now(); + return await ctx.db.insert("conversations", { + projectId, + title, + createdAt: now, + updatedAt: now, + }); + }, +}); + +// Get conversations for a project +export const getConversations = query({ + args: { + projectId: v.id("projects"), + }, + handler: async (ctx, { projectId }) => { + return await ctx.db + .query("conversations") + .withIndex("by_project", (q) => q.eq("projectId", projectId)) + .order("desc") + .collect(); + }, +}); + +// Get messages for a conversation +export const getMessages = query({ + args: { + conversationId: v.id("conversations"), + }, + handler: async (ctx, { conversationId }) => { + return await ctx.db + .query("messages") + .withIndex("by_conversation", (q) => q.eq("conversationId", conversationId)) + .order("asc") + .collect(); + }, +}); + +// Add a message to a conversation +export const addMessage = mutation({ + args: { + conversationId: v.id("conversations"), + role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")), + content: v.string(), + tools: v.optional(v.array(v.any())), + }, + handler: async (ctx, { conversationId, role, content, tools }) => { + const messageId = await ctx.db.insert("messages", { + conversationId, + role, + content, + timestamp: Date.now(), + tools, + }); + + // Update conversation's updatedAt timestamp + await ctx.db.patch(conversationId, { + updatedAt: Date.now(), + }); + + return messageId; + }, +}); + export const chatWithGrok = action({ args: { message: v.string(), context: v.string(), projectName: v.string(), + conversationHistory: v.optional(v.array(v.object({ + role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")), + content: v.string() + }))), }, - handler: async (ctx, { message, context, projectName }) => { + handler: async (ctx, { message, context, projectName, conversationHistory = [] }) => { // Get environment variables from Convex const apiKey = process.env.OPENROUTER_API_KEY; const siteUrl = process.env.SITE_URL || "https://open-superintelligence-lab-github-io.vercel.app"; @@ -24,12 +99,12 @@ export const chatWithGrok = action({ }); try { - const completion = await client.chat.completions.create({ - model: "x-ai/grok-4-fast:free", - messages: [ - { - role: "system", - content: `You are an AI research assistant for the "${projectName || 'Open Superintelligence Lab'}" project. You help users run experiments, analyze data, train models, and deploy them. + + // Build the messages array with system prompt, history, and current message + const messages = [ + { + role: "system" as const, + content: `You are an AI research assistant for the "${projectName || 'Open Superintelligence Lab'}" project. You help users run experiments, analyze data, train models, and deploy them. You have access to several tools: - run_experiment: Execute machine learning experiments @@ -41,13 +116,18 @@ When users ask about running experiments, analyzing data, training models, or de Current context: ${context || 'General research assistance'} -Respond naturally and helpfully to the user's request.` - }, - { - role: "user", - content: message as string - } - ], +Respond naturally and helpfully to the user's request. Remember previous messages in this conversation to provide context-aware responses.` + }, + ...conversationHistory, + { + role: "user" as const, + content: message as string + } + ]; + + const completion = await client.chat.completions.create({ + model: "x-ai/grok-4-fast:free", + messages, max_tokens: 1000, temperature: 0.7, }); diff --git a/convex/schema.ts b/convex/schema.ts index 8fc1dec..5cc980a 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -91,4 +91,27 @@ export default defineSchema({ }) .index("by_project", ["projectId"]) .index("by_service", ["serviceType"]), + + conversations: defineTable({ + projectId: v.id("projects"), + title: v.string(), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_project", ["projectId"]) + .index("by_updated", ["updatedAt"]), + + messages: defineTable({ + conversationId: v.id("conversations"), + role: v.union( + v.literal("user"), + v.literal("assistant"), + v.literal("system") + ), + content: v.string(), + timestamp: v.number(), + tools: v.optional(v.array(v.any())), + }) + .index("by_conversation", ["conversationId"]) + .index("by_conversation_and_timestamp", ["conversationId", "timestamp"]), }); From 2d79095dc71e6d1c02eb5952ff311fc66410ca27 Mon Sep 17 00:00:00 2001 From: vukrosic Date: Thu, 25 Sep 2025 19:00:13 +0200 Subject: [PATCH 02/30] Refactor Chatbot integration and enhance project schema - Replaced the existing Chatbot component with ChatGPTStyleChatbot for improved functionality. - Updated project detail and test pages to utilize the new ChatGPTStyleChatbot component. - Introduced new mutations for managing conversations, including deletion and title updates. - Expanded the schema to include tutorials and related tables for better content management. --- TUTORIAL_SYSTEM_README.md | 168 ++++++ app/api/ai/generate-tutorial/route.ts | 68 +++ app/projects/[id]/page.tsx | 10 +- app/test-chatbot/page.tsx | 4 +- app/tutorials/[id]/page.tsx | 18 + app/tutorials/create/page.tsx | 11 + app/tutorials/page.tsx | 11 + components/chatgpt-style-chatbot.tsx | 791 ++++++++++++++++++++++++++ components/navigation.tsx | 59 ++ components/tutorial-browser.tsx | 268 +++++++++ components/tutorial-editor.tsx | 384 +++++++++++++ components/tutorial-viewer.tsx | 305 ++++++++++ convex/_generated/api.d.ts | 4 + convex/chat.ts | 41 +- convex/schema.ts | 97 ++++ convex/tutorialChat.ts | 141 +++++ convex/tutorials.ts | 366 ++++++++++++ 17 files changed, 2738 insertions(+), 8 deletions(-) create mode 100644 TUTORIAL_SYSTEM_README.md create mode 100644 app/api/ai/generate-tutorial/route.ts create mode 100644 app/tutorials/[id]/page.tsx create mode 100644 app/tutorials/create/page.tsx create mode 100644 app/tutorials/page.tsx create mode 100644 components/chatgpt-style-chatbot.tsx create mode 100644 components/navigation.tsx create mode 100644 components/tutorial-browser.tsx create mode 100644 components/tutorial-editor.tsx create mode 100644 components/tutorial-viewer.tsx create mode 100644 convex/tutorialChat.ts create mode 100644 convex/tutorials.ts diff --git a/TUTORIAL_SYSTEM_README.md b/TUTORIAL_SYSTEM_README.md new file mode 100644 index 0000000..7746c43 --- /dev/null +++ b/TUTORIAL_SYSTEM_README.md @@ -0,0 +1,168 @@ +# Tutorial & Guide Creation System + +A comprehensive AI-powered tutorial and guide creation platform that allows users to create, edit, publish, and interact with tutorials through AI collaboration and chat functionality. + +## Features + +### 🎯 Core Functionality +- **AI-Powered Content Generation**: Generate tutorial content using AI based on prompts +- **Collaborative Editing**: Work with AI to create and refine tutorial content +- **Markdown Support**: Full Markdown support with syntax highlighting +- **Version Control**: Track changes and maintain tutorial versions +- **Publishing System**: Draft, publish, and archive tutorials +- **Permanent Storage**: Tutorials are saved forever once published + +### 💬 Interactive Features +- **Tutorial Chat**: Chat with tutorials to ask questions and get clarifications +- **Context-Aware Responses**: AI responses reference specific tutorial sections +- **Session Management**: Multiple chat sessions per tutorial +- **Real-time Collaboration**: Live editing and AI assistance + +### 🔍 Discovery & Browsing +- **Smart Search**: Search tutorials by content, title, tags, and categories +- **Advanced Filtering**: Filter by category, difficulty level, and tags +- **Trending & Popular**: Discover trending and popular tutorials +- **Statistics**: View counts, likes, and engagement metrics + +### 🎨 User Experience +- **Dark Theme**: Consistent dark theme across all components +- **Responsive Design**: Works on desktop and mobile devices +- **Real-time Preview**: Live preview of Markdown content +- **Intuitive Navigation**: Easy-to-use interface with clear navigation + +## Architecture + +### Database Schema +The system uses Convex for real-time data management with the following tables: + +- **tutorials**: Main tutorial content and metadata +- **tutorialVersions**: Version history for content tracking +- **tutorialCollaborations**: Multi-user collaboration support +- **tutorialComments**: Comments and discussion system +- **tutorialChatSessions**: Chat session management +- **tutorialChatMessages**: Individual chat messages + +### API Endpoints +- **AI Generation**: `/api/ai/generate-tutorial` - Generate content using OpenAI +- **Convex Functions**: Real-time data operations for tutorials and chat + +### Components +- **TutorialEditor**: AI-powered editor with collaboration features +- **TutorialViewer**: Reader with chat integration +- **TutorialBrowser**: Discovery and search interface +- **Navigation**: Consistent site navigation + +## Getting Started + +### Prerequisites +- Node.js 18+ +- Convex account and project +- OpenAI API key + +### Installation +1. Install dependencies: +```bash +npm install +``` + +2. Set up environment variables: +```bash +# Add to .env.local +OPENAI_API_KEY=your_openai_api_key +CONVEX_DEPLOYMENT=your_convex_deployment_url +``` + +3. Run Convex development server: +```bash +npx convex dev +``` + +4. Start the development server: +```bash +npm run dev +``` + +### Usage + +#### Creating a Tutorial +1. Navigate to `/tutorials/create` +2. Fill in basic information (title, description, category, difficulty) +3. Use the AI Assistant tab to generate content +4. Edit and refine the content in the Edit tab +5. Preview your tutorial in the Preview tab +6. Save as draft or publish immediately + +#### AI Content Generation +- Use natural language prompts to describe what you want to create +- AI generates comprehensive Markdown content +- Automatically extracts title, description, and tags +- Supports different difficulty levels and categories + +#### Chat with Tutorials +1. Open any published tutorial +2. Click the "Chat" button to start a conversation +3. Ask questions about specific sections +4. AI provides context-aware responses +5. Multiple chat sessions are supported + +#### Browsing Tutorials +1. Visit `/tutorials` to see all published tutorials +2. Use search to find specific content +3. Filter by category, difficulty, or tags +4. View trending and popular tutorials +5. Check statistics and engagement metrics + +## Technical Details + +### AI Integration +- Uses OpenAI GPT-4 for content generation +- Context-aware responses based on tutorial content +- Automatic metadata extraction (title, description, tags) +- Support for different writing styles and difficulty levels + +### Real-time Features +- Live collaboration using Convex +- Real-time chat functionality +- Instant updates and notifications +- Version control with change tracking + +### Security & Privacy +- User authentication integration ready +- Content ownership and permissions +- Private and public tutorial options +- Secure API key management + +## Future Enhancements + +### Planned Features +- **Multi-user Collaboration**: Real-time collaborative editing +- **Advanced AI Features**: Content improvement suggestions, SEO optimization +- **Rich Media Support**: Images, videos, and interactive content +- **Analytics Dashboard**: Detailed engagement and performance metrics +- **Export Options**: PDF, EPUB, and other format exports +- **Comment System**: Threaded discussions and feedback +- **Rating System**: User ratings and reviews +- **Recommendation Engine**: AI-powered content recommendations + +### Integration Opportunities +- **Authentication System**: User accounts and profiles +- **Payment Integration**: Premium features and monetization +- **Content Moderation**: AI-powered content filtering +- **API Access**: Third-party integrations and automation +- **Mobile App**: Native mobile applications + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +This project is part of the Open Superintelligence Lab initiative and follows the same licensing terms. + +--- + +**Built with ❤️ for the Open Superintelligence Lab community** diff --git a/app/api/ai/generate-tutorial/route.ts b/app/api/ai/generate-tutorial/route.ts new file mode 100644 index 0000000..cb82a75 --- /dev/null +++ b/app/api/ai/generate-tutorial/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import OpenAI from "openai"; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export async function POST(request: NextRequest) { + try { + const { prompt, existingContent, category, difficulty } = await request.json(); + + if (!prompt) { + return NextResponse.json({ error: "Prompt is required" }, { status: 400 }); + } + + const systemPrompt = `You are an expert tutorial writer. Create comprehensive, well-structured tutorials in Markdown format. + +Guidelines: +- Write clear, engaging content that's easy to follow +- Use proper Markdown formatting (headers, code blocks, lists, etc.) +- Include practical examples and code snippets when relevant +- Structure content with logical flow and progression +- Make it suitable for ${difficulty} level learners +- Focus on the ${category} domain +- Keep explanations clear and concise +- Include actionable steps and takeaways + +${existingContent ? `Build upon this existing content:\n\n${existingContent}\n\n` : ""} + +Generate a complete tutorial based on this request: ${prompt}`; + + const completion = await openai.chat.completions.create({ + model: "gpt-4", + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt } + ], + max_tokens: 8192, + temperature: 0.7, + }); + + const generatedContent = completion.choices[0]?.message?.content || ""; + + // Extract title and description from the generated content + const lines = generatedContent.split('\n'); + const title = lines.find(line => line.startsWith('# '))?.replace('# ', '') || 'Generated Tutorial'; + const description = lines.find(line => line.startsWith('## ') || line.startsWith('### '))?.replace(/^#+\s*/, '') || 'AI-generated tutorial content'; + + // Extract tags from content (simple keyword extraction) + const tagKeywords = ['tutorial', 'guide', 'how-to', 'step-by-step', 'beginner', 'advanced', 'example', 'code', 'implementation']; + const contentWords = generatedContent.toLowerCase().split(/\s+/); + const extractedTags = tagKeywords.filter(keyword => contentWords.includes(keyword)).slice(0, 5); + + return NextResponse.json({ + content: generatedContent, + title, + description, + tags: extractedTags, + }); + + } catch (error) { + console.error("Error generating tutorial content:", error); + return NextResponse.json( + { error: "Failed to generate tutorial content" }, + { status: 500 } + ); + } +} diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx index 20ef24b..c7d8552 100644 --- a/app/projects/[id]/page.tsx +++ b/app/projects/[id]/page.tsx @@ -12,7 +12,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import { ArrowLeft, Play, Pause, Square, Settings, GitBranch, ExternalLink, Activity, Terminal, Clock } from 'lucide-react'; import Link from 'next/link'; import { AppLayout } from '@/components/layout/app-layout'; -import Chatbot from '@/components/chatbot'; +import ChatGPTStyleChatbot from '@/components/chatgpt-style-chatbot'; import Canvas from '@/components/canvas'; export default function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) { @@ -40,7 +40,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st // Convex queries - skip for now to test interface const project = mockProject; - const runs = []; + const runs: any[] = []; const updateRunStatus = useMutation(api.runs.updateStatus); const deleteRun = useMutation(api.runs.remove); @@ -127,8 +127,8 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st {/* Chatbot Tab */} {resolvedParams && ( - )} @@ -298,7 +298,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st runs.map((run) => ( - + {run.name} diff --git a/app/test-chatbot/page.tsx b/app/test-chatbot/page.tsx index d2e090b..6f15cd0 100644 --- a/app/test-chatbot/page.tsx +++ b/app/test-chatbot/page.tsx @@ -1,7 +1,7 @@ 'use client'; import React from 'react'; -import Chatbot from '@/components/chatbot'; +import ChatGPTStyleChatbot from '@/components/chatgpt-style-chatbot'; import Canvas from '@/components/canvas'; import { AppLayout } from '@/components/layout/app-layout'; @@ -13,7 +13,7 @@ export default function TestChatbot() {

AI Assistant

- +

Results Canvas

diff --git a/app/tutorials/[id]/page.tsx b/app/tutorials/[id]/page.tsx new file mode 100644 index 0000000..b866501 --- /dev/null +++ b/app/tutorials/[id]/page.tsx @@ -0,0 +1,18 @@ +import { TutorialViewer } from "@/components/tutorial-viewer"; +import { Navigation } from "@/components/navigation"; +import { Id } from "@/convex/_generated/dataModel"; + +interface TutorialPageProps { + params: { + id: string; + }; +} + +export default function TutorialPage({ params }: TutorialPageProps) { + return ( +
+ + } /> +
+ ); +} diff --git a/app/tutorials/create/page.tsx b/app/tutorials/create/page.tsx new file mode 100644 index 0000000..c6e8c74 --- /dev/null +++ b/app/tutorials/create/page.tsx @@ -0,0 +1,11 @@ +import { TutorialEditor } from "@/components/tutorial-editor"; +import { Navigation } from "@/components/navigation"; + +export default function CreateTutorialPage() { + return ( +
+ + +
+ ); +} diff --git a/app/tutorials/page.tsx b/app/tutorials/page.tsx new file mode 100644 index 0000000..70b5798 --- /dev/null +++ b/app/tutorials/page.tsx @@ -0,0 +1,11 @@ +import { TutorialBrowser } from "@/components/tutorial-browser"; +import { Navigation } from "@/components/navigation"; + +export default function TutorialsPage() { + return ( +
+ + +
+ ); +} diff --git a/components/chatgpt-style-chatbot.tsx b/components/chatgpt-style-chatbot.tsx new file mode 100644 index 0000000..104e8c9 --- /dev/null +++ b/components/chatgpt-style-chatbot.tsx @@ -0,0 +1,791 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { useAction, useMutation, useQuery } from 'convex/react'; +import { api } from '../convex/_generated/api'; +import { Id } from '../convex/_generated/dataModel'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; +import { + Send, Bot, User, Loader2, Play, Pause, Square, CheckCircle, AlertCircle, Copy, Check, + Plus, Trash2, MessageSquare, MoreHorizontal, Edit3, X, Menu, ChevronLeft, ChevronRight +} from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeHighlight from 'rehype-highlight'; + +interface Message { + id: string; + type: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; + isTyping?: boolean; + tools?: ToolExecution[]; +} + +interface ToolExecution { + id: string; + name: string; + status: 'pending' | 'running' | 'completed' | 'failed'; + result?: any; + error?: string; +} + +interface ChatbotProps { + projectId: Id<"projects">; + projectName: string; +} + +// Mock MCP Tools +const mockTools = { + 'run_experiment': { + name: 'Run Experiment', + description: 'Execute a machine learning experiment', + execute: async (params: any) => { + await new Promise(resolve => setTimeout(resolve, 2000)); + return { + experimentId: `exp_${Date.now()}`, + status: 'running', + progress: 0, + estimatedTime: '2-3 hours', + gpuAllocated: 'A100 x 2' + }; + } + }, + 'analyze_data': { + name: 'Analyze Data', + description: 'Perform data analysis and visualization', + execute: async (params: any) => { + await new Promise(resolve => setTimeout(resolve, 1500)); + return { + analysisId: `analysis_${Date.now()}`, + insights: ['Data shows normal distribution', 'Outliers detected in 3% of samples'], + charts: ['distribution_plot.png', 'correlation_matrix.png'] + }; + } + }, + 'train_model': { + name: 'Train Model', + description: 'Train a machine learning model', + execute: async (params: any) => { + await new Promise(resolve => setTimeout(resolve, 3000)); + return { + modelId: `model_${Date.now()}`, + accuracy: 0.94, + loss: 0.12, + epochs: 50, + trainingTime: '45 minutes' + }; + } + }, + 'deploy_model': { + name: 'Deploy Model', + description: 'Deploy model to production', + execute: async (params: any) => { + await new Promise(resolve => setTimeout(resolve, 1000)); + return { + deploymentId: `deploy_${Date.now()}`, + endpoint: 'https://api.example.com/model/predict', + status: 'active', + latency: '45ms' + }; + } + }, + 'create_colab_notebook': { + name: 'Create Colab Notebook', + description: 'Generate and create a Google Colab notebook with code', + execute: async (params: any) => { + await new Promise(resolve => setTimeout(resolve, 1500)); + + const notebookId = `colab_${Date.now()}`; + const colabUrl = `https://colab.research.google.com/drive/${notebookId}`; + + const notebookContent = { + cells: [ + { + cell_type: 'markdown', + source: ['# AI Generated Notebook\n', 'Created by AI Research Assistant\n', `Generated at: ${new Date().toLocaleString()}`] + }, + { + cell_type: 'code', + source: params.code || ['# Add your code here\n', 'print("Hello from AI-generated Colab notebook!")'] + } + ], + metadata: { + accelerator: 'GPU', + colab: { + name: params.title || 'AI Generated Notebook', + version: '0.3.2' + } + } + }; + + return { + notebookId, + colabUrl, + title: params.title || 'AI Generated Notebook', + status: 'created', + message: 'Notebook created successfully! Click the link to open in Google Colab.', + cells: notebookContent.cells.length, + hasCode: true + }; + } + } +}; + +export default function ChatGPTStyleChatbot({ projectId, projectName }: ChatbotProps) { + const [messages, setMessages] = useState([]); + const [inputMessage, setInputMessage] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [copiedCodeBlocks, setCopiedCodeBlocks] = useState>(new Set()); + const [copiedMessages, setCopiedMessages] = useState>(new Set()); + const [currentConversationId, setCurrentConversationId] = useState | null>(null); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [editingConversationId, setEditingConversationId] = useState | null>(null); + const [editingTitle, setEditingTitle] = useState(''); + const scrollAreaRef = useRef(null); + + // Convex hooks + const chatWithGrok = useAction(api.chat.chatWithGrok); + const createConversation = useMutation(api.chat.createConversation); + const addMessage = useMutation(api.chat.addMessage); + const deleteConversation = useMutation(api.chat.deleteConversation); + const updateConversationTitle = useMutation(api.chat.updateConversationTitle); + const conversations = useQuery(api.chat.getConversations, { projectId }); + const conversationMessages = useQuery( + api.chat.getMessages, + currentConversationId ? { conversationId: currentConversationId } : "skip" + ); + + const scrollToBottom = () => { + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // Load conversation messages when conversation changes + useEffect(() => { + if (conversationMessages) { + const formattedMessages: Message[] = conversationMessages.map(msg => ({ + id: msg._id, + type: msg.role as 'user' | 'assistant' | 'system', + content: msg.content, + timestamp: new Date(msg.timestamp), + tools: msg.tools + })); + setMessages(formattedMessages); + } + }, [conversationMessages]); + + // Create initial conversation if none exists + useEffect(() => { + if (!currentConversationId && conversations && conversations.length === 0) { + createConversation({ + projectId, + title: `New Chat` + }).then(conversationId => { + setCurrentConversationId(conversationId); + // Add welcome message + addMessage({ + conversationId, + role: 'assistant', + content: `Hello! I'm your AI research assistant for the "${projectName}" project. + +I can help you with: +- **General questions** about your research and project +- **Running experiments** (say "run experiment" to use tools) +- **Data analysis** (say "analyze data" to use tools) +- **Model training** (say "train model" to use tools) +- **Model deployment** (say "deploy model" to use tools) +- **Google Colab notebooks** (say "create colab notebook" to generate notebooks) + +I'll only use tools when you explicitly ask me to run experiments or use MCP tools. Otherwise, I'll just chat and provide guidance. + +What would you like to work on today?` + }); + }); + } else if (conversations && conversations.length > 0 && !currentConversationId) { + // Load the most recent conversation + setCurrentConversationId(conversations[0]._id); + } + }, [conversations, currentConversationId, projectId, projectName, createConversation, addMessage]); + + const handleNewChat = async () => { + const conversationId = await createConversation({ + projectId, + title: `New Chat` + }); + setCurrentConversationId(conversationId); + setMessages([]); + }; + + const handleDeleteConversation = async (conversationId: Id<"conversations">) => { + if (confirm('Are you sure you want to delete this conversation?')) { + await deleteConversation({ conversationId }); + if (currentConversationId === conversationId) { + const remainingConversations = conversations?.filter(c => c._id !== conversationId); + if (remainingConversations && remainingConversations.length > 0) { + setCurrentConversationId(remainingConversations[0]._id); + } else { + setCurrentConversationId(null); + setMessages([]); + } + } + } + }; + + const handleEditTitle = (conversationId: Id<"conversations">, currentTitle: string) => { + setEditingConversationId(conversationId); + setEditingTitle(currentTitle); + }; + + const handleSaveTitle = async () => { + if (editingConversationId && editingTitle.trim()) { + await updateConversationTitle({ + conversationId: editingConversationId, + title: editingTitle.trim() + }); + setEditingConversationId(null); + setEditingTitle(''); + } + }; + + const handleCancelEdit = () => { + setEditingConversationId(null); + setEditingTitle(''); + }; + + const copyToClipboard = async (text: string, type: 'code' | 'message', id: string) => { + try { + await navigator.clipboard.writeText(text); + if (type === 'code') { + setCopiedCodeBlocks(prev => new Set([...prev, id])); + setTimeout(() => { + setCopiedCodeBlocks(prev => { + const newSet = new Set(prev); + newSet.delete(id); + return newSet; + }); + }, 2000); + } else { + setCopiedMessages(prev => new Set([...prev, id])); + setTimeout(() => { + setCopiedMessages(prev => { + const newSet = new Set(prev); + newSet.delete(id); + return newSet; + }); + }, 2000); + } + } catch (err) { + console.error('Failed to copy text: ', err); + } + }; + + const handleSendMessage = async () => { + if (!inputMessage.trim() || isLoading || !currentConversationId) return; + + const userMessage: Message = { + id: Date.now().toString(), + type: 'user', + content: inputMessage, + timestamp: new Date() + }; + + setMessages(prev => [...prev, userMessage]); + + // Save user message to database + await addMessage({ + conversationId: currentConversationId, + role: 'user', + content: inputMessage + }); + + const messageToSend = inputMessage; + setInputMessage(''); + setIsLoading(true); + + try { + // Check if user explicitly wants to run experiments or use tools + const shouldUseTools = messageToSend.toLowerCase().includes('run experiment') || + messageToSend.toLowerCase().includes('use mcp') || + messageToSend.toLowerCase().includes('run tool') || + messageToSend.toLowerCase().includes('execute') || + messageToSend.toLowerCase().includes('train model') || + messageToSend.toLowerCase().includes('analyze data') || + messageToSend.toLowerCase().includes('deploy model') || + messageToSend.toLowerCase().includes('create colab') || + messageToSend.toLowerCase().includes('colab notebook') || + messageToSend.toLowerCase().includes('generate notebook') || + messageToSend.toLowerCase().includes('open colab'); + + // Prepare conversation history for the AI + const conversationHistory = messages.slice(0, -1).map(msg => ({ + role: msg.type as 'user' | 'assistant' | 'system', + content: msg.content + })); + + const response = await chatWithGrok({ + message: messageToSend, + context: projectName, + projectName: projectName, + conversationHistory + }); + + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + type: 'assistant', + content: response.response, + timestamp: new Date(), + tools: shouldUseTools && response.tools ? response.tools.map((toolName: string) => ({ + id: `${toolName}_${Date.now()}`, + name: mockTools[toolName as keyof typeof mockTools].name, + status: 'pending' as const + })) : [] + }; + + setMessages(prev => [...prev, assistantMessage]); + + // Save assistant message to database + await addMessage({ + conversationId: currentConversationId, + role: 'assistant', + content: response.response, + tools: shouldUseTools && response.tools ? response.tools.map((toolName: string) => ({ + id: `${toolName}_${Date.now()}`, + name: mockTools[toolName as keyof typeof mockTools].name, + status: 'pending' as const + })) : [] + }); + + // Execute tools if any and user explicitly requested them + if (shouldUseTools && response.tools && response.tools.length > 0) { + for (const toolName of response.tools) { + const tool = mockTools[toolName as keyof typeof mockTools]; + if (tool) { + // Update tool status to running + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + tools: msg.tools?.map(t => + t.name === tool.name + ? { ...t, status: 'running' as const } + : t + ) + } + : msg + )); + + try { + const result = await tool.execute(response.toolParams); + + // Update tool status to completed + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + tools: msg.tools?.map(t => + t.name === tool.name + ? { ...t, status: 'completed' as const, result } + : t + ) + } + : msg + )); + + // Add result message + const resultContent = tool.name === 'Create Colab Notebook' && (result as any).colabUrl + ? `✅ ${tool.name} completed successfully!\n\n📓 **Notebook Created**: ${(result as any).title}\n🔗 **Colab Link**: [Open in Google Colab](${(result as any).colabUrl})\n\nClick the link above to open your notebook in Google Colab and start running your code!` + : `✅ ${tool.name} completed successfully!`; + + const resultMessage: Message = { + id: (Date.now() + 2).toString(), + type: 'system', + content: resultContent, + timestamp: new Date() + }; + setMessages(prev => [...prev, resultMessage]); + + // Save result message to database + await addMessage({ + conversationId: currentConversationId, + role: 'system', + content: resultContent + }); + + } catch (error) { + // Update tool status to failed + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + tools: msg.tools?.map(t => + t.name === tool.name + ? { ...t, status: 'failed' as const, error: error instanceof Error ? error.message : String(error) } + : t + ) + } + : msg + )); + } + } + } + } + + } catch (error) { + const errorContent = "I'm sorry, I encountered an error. Please try again."; + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + type: 'assistant', + content: errorContent, + timestamp: new Date() + }; + setMessages(prev => [...prev, errorMessage]); + + // Save error message to database + if (currentConversationId) { + await addMessage({ + conversationId: currentConversationId, + role: 'assistant', + content: errorContent + }); + } + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const getToolIcon = (status: string) => { + switch (status) { + case 'pending': return ; + case 'running': return ; + case 'completed': return ; + case 'failed': return ; + default: return ; + } + }; + + const formatConversationTitle = (title: string) => { + return title.length > 30 ? title.substring(0, 30) + '...' : title; + }; + + return ( +
+ {/* Sidebar */} +
+
+
+

Chat History

+ +
+
+ + +
+ {conversations?.map((conversation) => ( +
setCurrentConversationId(conversation._id)} + > + {editingConversationId === conversation._id ? ( +
+ setEditingTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveTitle(); + if (e.key === 'Escape') handleCancelEdit(); + }} + className="h-6 text-xs" + autoFocus + /> + + +
+ ) : ( +
+
+ + + {formatConversationTitle(conversation.title)} + +
+
+ + +
+
+ )} +
+ {new Date(conversation.updatedAt).toLocaleDateString()} +
+
+ ))} +
+
+
+ + {/* Main Chat Area */} +
+ {/* Header */} +
+
+ +
+

AI Research Assistant

+

{projectName}

+
+
+ +
+ + {/* Messages */} + +
+ {messages.map((message) => ( +
+
+
+
+ {message.type === 'user' ? ( + + ) : message.type === 'assistant' ? ( + + ) : ( + + )} + + {message.timestamp.toLocaleTimeString()} + +
+ +
+
+ { + const match = /language-(\w+)/.exec(className || ''); + const codeId = `${message.id}_${Date.now()}_${Math.random()}`; + + if (!inline && match) { + const isCopied = copiedCodeBlocks.has(codeId); + + const handleCopyCode = () => { + const codeElement = document.querySelector(`[data-code-id="${codeId}"]`); + if (codeElement) { + const textContent = codeElement.textContent || ''; + copyToClipboard(textContent, 'code', codeId); + } + }; + + return ( +
+
+                                  
+                                    {children}
+                                  
+                                
+ +
+ ); + } + + return ( + + {children} + + ); + }, + pre: ({ children }) => <>{children}, + p: ({ children }) =>

{children}

, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + blockquote: ({ children }) =>
    {children}
    , + table: ({ children }) => {children}
    , + th: ({ children }) => {children}, + td: ({ children }) => {children}, + }} + > + {message.content} +
    +
    + + {/* Tool executions */} + {message.tools && message.tools.length > 0 && ( +
    + {message.tools.map((tool) => ( +
    + {getToolIcon(tool.status)} + {tool.name} + + {tool.status} + + {tool.result && ( +
    + {tool.name === 'Create Colab Notebook' && tool.result.colabUrl ? ( + + Open Colab Notebook → + + ) : ( + JSON.stringify(tool.result).substring(0, 50) + '...' + )} +
    + )} +
    + ))} +
    + )} +
    +
    + ))} + + {isLoading && ( +
    +
    +
    + + + AI is thinking... +
    +
    +
    + )} +
    +
    + + {/* Input Area */} +
    +
    +
    + setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Ask questions or say 'run experiment' to use tools..." + disabled={isLoading} + className="flex-1" + /> + +
    +
    +
    +
    +
    + ); +} diff --git a/components/navigation.tsx b/components/navigation.tsx new file mode 100644 index 0000000..1c292ba --- /dev/null +++ b/components/navigation.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; + +interface NavigationProps { + currentPath?: string; +} + +export function Navigation({ currentPath }: NavigationProps) { + return ( +
    + +
    + ); +} diff --git a/components/tutorial-browser.tsx b/components/tutorial-browser.tsx new file mode 100644 index 0000000..ab5fd43 --- /dev/null +++ b/components/tutorial-browser.tsx @@ -0,0 +1,268 @@ +"use client"; + +import { useState } from "react"; +import { useQuery } from "convex/react"; +import { api } from "@/convex/_generated/api"; +import { Id } from "@/convex/_generated/dataModel"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Search, + Plus, + Eye, + Heart, + Clock, + Tag, + Filter, + BookOpen, + TrendingUp, + Star +} from "lucide-react"; +import Link from "next/link"; + +export function TutorialBrowser() { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState(""); + const [selectedDifficulty, setSelectedDifficulty] = useState(""); + const [activeTab, setActiveTab] = useState("all"); + + const publishedTutorials = useQuery(api.tutorials.getPublishedTutorials, { + category: selectedCategory || undefined, + difficulty: selectedDifficulty as any || undefined, + }); + + const searchResults = useQuery( + api.tutorials.searchTutorials, + searchQuery ? { + query: searchQuery, + category: selectedCategory || undefined, + difficulty: selectedDifficulty as any || undefined, + } : "skip" + ); + + const tutorials = searchQuery ? searchResults : publishedTutorials; + + const categories = [ + "Programming", + "Design", + "Business", + "Marketing", + "Data Science", + "Machine Learning", + "Web Development", + "Mobile Development", + "DevOps", + "Other" + ]; + + const difficulties = [ + { value: "beginner", label: "Beginner" }, + { value: "intermediate", label: "Intermediate" }, + { value: "advanced", label: "Advanced" } + ]; + + const getDifficultyColor = (difficulty: string) => { + switch (difficulty) { + case "beginner": return "bg-green-100 text-green-800"; + case "intermediate": return "bg-yellow-100 text-yellow-800"; + case "advanced": return "bg-red-100 text-red-800"; + default: return "bg-gray-100 text-gray-800"; + } + }; + + return ( +
    + {/* Header */} +
    +
    +

    Tutorial Library

    +

    + Discover and learn from AI-generated tutorials and guides +

    +
    + + + +
    + + {/* Search and Filters */} + + +
    +
    + + setSearchQuery(e.target.value)} + className="pl-10" + /> +
    + + +
    + + + + All Tutorials + Trending + Recent + Most Popular + + +
    +
    + + {/* Results */} +
    + {tutorials?.map((tutorial) => ( + + +
    + + {tutorial.category} + + + {tutorial.difficulty} + +
    + + + {tutorial.title} + + +
    + +

    + {tutorial.description} +

    + +
    + {tutorial.tags.slice(0, 3).map((tag) => ( + + + {tag} + + ))} + {tutorial.tags.length > 3 && ( + + +{tutorial.tags.length - 3} more + + )} +
    + +
    +
    +
    + + {tutorial.views} +
    +
    + + {tutorial.likes} +
    +
    + + {tutorial.estimatedReadTime}m +
    +
    +
    + {tutorial.aiGenerated && ( + + )} +
    +
    +
    +
    + ))} +
    + + {/* Empty State */} + {tutorials?.length === 0 && ( +
    + +

    No tutorials found

    +

    + {searchQuery + ? "Try adjusting your search terms or filters" + : "Be the first to create a tutorial!" + } +

    + + + +
    + )} + + {/* Stats */} + {tutorials && tutorials.length > 0 && ( +
    + + +
    + {tutorials.length} +
    +
    Total Tutorials
    +
    +
    + + +
    + {tutorials.reduce((sum, t) => sum + t.views, 0)} +
    +
    Total Views
    +
    +
    + + +
    + {tutorials.reduce((sum, t) => sum + t.likes, 0)} +
    +
    Total Likes
    +
    +
    +
    + )} +
    + ); +} diff --git a/components/tutorial-editor.tsx b/components/tutorial-editor.tsx new file mode 100644 index 0000000..746a603 --- /dev/null +++ b/components/tutorial-editor.tsx @@ -0,0 +1,384 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useMutation, useQuery } from "convex/react"; +import { api } from "@/convex/_generated/api"; +import { Id } from "@/convex/_generated/dataModel"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Save, + Eye, + Send, + Bot, + User, + Plus, + X, + Upload, + Archive, + History, + MessageSquare, + Sparkles +} from "lucide-react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeHighlight from "rehype-highlight"; + +interface TutorialEditorProps { + tutorialId?: Id<"tutorials">; + onSave?: (tutorialId: Id<"tutorials">) => void; + onPublish?: (tutorialId: Id<"tutorials">) => void; +} + +export function TutorialEditor({ tutorialId, onSave, onPublish }: TutorialEditorProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [content, setContent] = useState(""); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState(""); + const [category, setCategory] = useState(""); + const [difficulty, setDifficulty] = useState<"beginner" | "intermediate" | "advanced">("beginner"); + const [isPublic, setIsPublic] = useState(false); + const [aiPrompt, setAiPrompt] = useState(""); + const [isGenerating, setIsGenerating] = useState(false); + const [activeTab, setActiveTab] = useState("edit"); + + const createTutorial = useMutation(api.tutorials.createTutorial); + const updateTutorial = useMutation(api.tutorials.updateTutorial); + const publishTutorial = useMutation(api.tutorials.publishTutorial); + const getTutorial = useQuery(api.tutorials.getTutorial, tutorialId ? { id: tutorialId } : "skip"); + + // Load existing tutorial data + useEffect(() => { + if (getTutorial) { + setTitle(getTutorial.title); + setDescription(getTutorial.description); + setContent(getTutorial.content); + setTags(getTutorial.tags); + setCategory(getTutorial.category); + setDifficulty(getTutorial.difficulty); + setIsPublic(getTutorial.isPublic); + } + }, [getTutorial]); + + const handleSave = async () => { + try { + let savedTutorialId = tutorialId; + + if (!tutorialId) { + // Create new tutorial + savedTutorialId = await createTutorial({ + title, + description, + content, + authorId: "user-123", // TODO: Get from auth + tags, + category, + difficulty, + isPublic, + aiGenerated: false, + }); + } else { + // Update existing tutorial + await updateTutorial({ + id: tutorialId, + title, + description, + content, + tags, + category, + difficulty, + isPublic, + changeDescription: "Manual edit", + }); + } + + onSave?.(savedTutorialId); + } catch (error) { + console.error("Failed to save tutorial:", error); + } + }; + + const handlePublish = async () => { + try { + await handleSave(); + if (tutorialId) { + await publishTutorial({ id: tutorialId }); + onPublish?.(tutorialId); + } + } catch (error) { + console.error("Failed to publish tutorial:", error); + } + }; + + const handleAiGenerate = async () => { + if (!aiPrompt.trim()) return; + + setIsGenerating(true); + try { + // TODO: Integrate with AI service + const response = await fetch("/api/ai/generate-tutorial", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + prompt: aiPrompt, + existingContent: content, + category, + difficulty, + }), + }); + + const data = await response.json(); + + if (data.content) { + setContent(data.content); + if (data.title) setTitle(data.title); + if (data.description) setDescription(data.description); + if (data.tags) setTags(data.tags); + } + } catch (error) { + console.error("Failed to generate content:", error); + } finally { + setIsGenerating(false); + } + }; + + const addTag = () => { + if (newTag.trim() && !tags.includes(newTag.trim())) { + setTags([...tags, newTag.trim()]); + setNewTag(""); + } + }; + + const removeTag = (tagToRemove: string) => { + setTags(tags.filter(tag => tag !== tagToRemove)); + }; + + return ( +
    +
    +

    + {tutorialId ? "Edit Tutorial" : "Create New Tutorial"} +

    +
    + + +
    +
    + + + + Edit + Preview + AI Assistant + + + + + + Basic Information + + +
    + + setTitle(e.target.value)} + placeholder="Enter tutorial title..." + /> +
    + +
    + +