diff --git a/src/main/kotlin/com/config/GlobalConfig.kt b/src/main/kotlin/com/config/GlobalConfig.kt index 8faa701..7918e9a 100644 --- a/src/main/kotlin/com/config/GlobalConfig.kt +++ b/src/main/kotlin/com/config/GlobalConfig.kt @@ -9,5 +9,5 @@ object GlobalConfig { // Default chat model (OpenRouter identifier) // Using a model with larger context window for better guide generation @JvmStatic - var model_name: String = "mistralai/mistral-small-3.2-24b-instruct:free" + var model_name: String = "openai/gpt-oss-20b:free" } diff --git a/src/main/kotlin/com/gitdiff/CodingTaskService.kt b/src/main/kotlin/com/gitdiff/CodingTaskService.kt new file mode 100644 index 0000000..33c224f --- /dev/null +++ b/src/main/kotlin/com/gitdiff/CodingTaskService.kt @@ -0,0 +1,45 @@ +package com.gitdiff + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project + +/** + * Project-level service that surfaces the CodingTasks associated with the Guide. + * + * This service intentionally delegates to GuideService to avoid creating a + * separate CodingTask instance and to ensure persistence across the project + * lifecycle. The Guide remains the single source of truth, and we update the + * coding tasks by copying the Guide with new CodingTasks when needed. + */ +@Service(Service.Level.PROJECT) +class CodingTaskService(private val project: Project) { + + private val guideService: GuideService = GuideService.getInstance(project) + + fun getCodingTasks(): CodingTaskList = guideService.getGuide().codingTasks + + fun setCodingTasks(newCodingTasks: CodingTaskList) { + val current = guideService.getGuide() + val updated = current.copy(codingTasks = newCodingTasks) + guideService.setGuide(updated) + } + + // Convenience accessors + fun getTasks(): List = getCodingTasks().tasks + fun getFirstTask(): CodingTask? = getCodingTasks().tasks.firstOrNull() + + /** + * Check if coding tasks have been generated + */ + fun hasCodingTasksContent(): Boolean = getCodingTasks().tasks.isNotEmpty() + + /** + * Get coding tasks count + */ + fun getCodingTasksCount(): Int = getCodingTasks().tasks.size + + companion object { + fun getInstance(project: Project): CodingTaskService = project.service() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/gitdiff/Models.kt b/src/main/kotlin/com/gitdiff/Models.kt index 5723fe1..08386c3 100644 --- a/src/main/kotlin/com/gitdiff/Models.kt +++ b/src/main/kotlin/com/gitdiff/Models.kt @@ -53,7 +53,8 @@ data class Guide( val commitDiff: CommitDiff, var content: String, val chat: MutableList, - val quiz : Quiz + val quiz : Quiz, + val codingTasks: CodingTaskList = CodingTaskList(emptyList()) ) data class Quiz( @@ -70,3 +71,14 @@ data class QuizOption( val label: String, val isCorrect: Boolean ) + +data class CodingTask( + val id: Int, + val title: String, + val languageId: String, // e.g., "kotlin", "java", "python" + val initialCode: String +) + +data class CodingTaskList( + val tasks: List +) diff --git a/src/main/kotlin/com/quiz/cards/TaskCard.kt b/src/main/kotlin/com/quiz/cards/TaskCard.kt index 4686e41..7ef9a8c 100644 --- a/src/main/kotlin/com/quiz/cards/TaskCard.kt +++ b/src/main/kotlin/com/quiz/cards/TaskCard.kt @@ -30,13 +30,9 @@ import java.security.MessageDigest import javax.swing.* import kotlin.random.Random -// Если у тебя CodingTask внутри QuizBank, импортни её как ниже: -// import com.github.yaroslavmayorov.testingpluginui.quiz.data.QuizBank -// и потом используй QuizBank.CodingTask -// Или, если вынесена модель, просто замени тип ниже на твой data class. class TaskCard( private val project: Project, - private val task: QuizBank.CodingTask + private val task: com.gitdiff.CodingTask ) : JPanel() { private val roundedBorder = RoundedLineBorder(JBColor.border(), arc = JBUI.scale(8), thickness = JBUI.scale(1)) @@ -167,7 +163,7 @@ class TaskCard( // ---------- UI parts ---------- - private fun buildHeader(t: QuizBank.CodingTask): JComponent { + private fun buildHeader(t: com.gitdiff.CodingTask): JComponent { val pnl = JPanel(GridBagLayout()).apply { isOpaque = false } val c = GridBagConstraints().apply { gridy = 0 diff --git a/src/main/kotlin/com/quiz/pages/ChatPage.kt b/src/main/kotlin/com/quiz/pages/ChatPage.kt index cfed901..f85f32b 100644 --- a/src/main/kotlin/com/quiz/pages/ChatPage.kt +++ b/src/main/kotlin/com/quiz/pages/ChatPage.kt @@ -183,7 +183,7 @@ class ChatPage( }.start() } - // Method to generate quiz when triggered from outside + // Method to generate quiz and coding tasks when triggered from outside fun generateQuiz() { // Check if guide exists first val guideService = com.gitdiff.GuideService.getInstance(project) @@ -197,24 +197,34 @@ class ChatPage( return } - // Generate quiz in background thread to avoid blocking UI + // Generate quiz and coding tasks in background thread to avoid blocking UI Thread { try { + // Generate both quiz and coding tasks in parallel for better performance val quizSuccess = QuizGenerator.generateQuizFromGuide(project) + val codingTaskSuccess = llm_pipeline.CodingTaskGenerator.generateCodingTasksFromGuide(project) SwingUtilities.invokeLater { - if (quizSuccess) { - // Quiz generated successfully, refresh quiz page and proceed - quizPage?.refreshQuizContent() + if (quizSuccess || codingTaskSuccess) { + // At least one generation was successful, refresh quiz page and proceed + quizPage?.refreshQuizContent(project) onGoQuiz() + + // Log results for debugging + if (!quizSuccess) { + println("Quiz generation failed, but coding tasks generated successfully") + } + if (!codingTaskSuccess) { + println("Coding task generation failed, but quiz generated successfully") + } } else { - // Handle quiz generation failure - println("Failed to generate quiz from guide") + // Handle both generation failures + println("Failed to generate both quiz and coding tasks from guide") } } } catch (e: Exception) { SwingUtilities.invokeLater { - println("Error generating quiz: ${e.message}") + println("Error generating quiz and coding tasks: ${e.message}") } } }.start() diff --git a/src/main/kotlin/com/quiz/pages/QuizPage.kt b/src/main/kotlin/com/quiz/pages/QuizPage.kt index add1e14..3532d4b 100644 --- a/src/main/kotlin/com/quiz/pages/QuizPage.kt +++ b/src/main/kotlin/com/quiz/pages/QuizPage.kt @@ -18,6 +18,7 @@ import javax.swing.ScrollPaneConstants import java.awt.Cursor import javax.swing.JButton import com.gitdiff.QuizService +import com.gitdiff.CodingTaskService class QuizPage( project: Project, @@ -34,6 +35,7 @@ class QuizPage( private val backBtn: JButton private val themeChildren = mutableListOf() private val quizService = QuizService.getInstance(project) + private val codingTaskService = CodingTaskService.getInstance(project) init { border = null @@ -72,8 +74,64 @@ class QuizPage( } content.add(header) - // задачи - QuizBank.codingTasks.forEach { content.add(TaskCard(project, it)) } + // Load coding tasks + loadCodingTasks(project) + } + + private fun loadCodingTasks(project: Project) { + // Get coding tasks from service + val codingTasks = codingTaskService.getTasks() + + if (codingTasks.isEmpty()) { + // Check if we have guide content to generate coding tasks + val guideService = com.gitdiff.GuideService.getInstance(project) + val guide = guideService.getGuide() + + if (guide.content.isNotBlank()) { + // Show generating message and try to generate coding tasks in background + val generatingLabel = JLabel("
" + + "

Generating Coding Tasks...

" + + "

Creating practical programming exercises based on your guide content.

" + + "
").apply { + font = JBFont.regular() + foreground = JBColor.gray + border = JBUI.Borders.empty(20) + } + content.add(generatingLabel) + } else { + // No guide content available + val noCodingTasksLabel = JLabel("
" + + "

No Coding Tasks Available

" + + "

Coding tasks will be generated from the guide content. Please generate a guide first using the Tools menu.

" + + "
").apply { + font = JBFont.regular() + foreground = JBColor.gray + border = JBUI.Borders.empty(20) + } + content.add(noCodingTasksLabel) + } + + // Fallback to static tasks for now (as examples) + val fallbackHeader = JLabel("Example Tasks (Static)").apply { + font = JBFont.label().asBold() + border = JBUI.Borders.emptyTop(16) + foreground = JBColor.gray + } + content.add(fallbackHeader) + + QuizBank.codingTasks.forEach { task -> + val convertedTask = com.gitdiff.CodingTask( + id = task.id, + title = task.title, + languageId = task.languageId, + initialCode = task.initialCode + ) + content.add(TaskCard(project, convertedTask)) + } + } else { + // Use generated coding tasks + codingTasks.forEach { content.add(TaskCard(project, it)) } + } } private fun loadQuizQuestions(questions: List) { @@ -124,24 +182,26 @@ class QuizPage( backBtn.requestFocusInWindow() } - fun refreshQuizContent() { - // Remove existing quiz cards but keep back button and fixed content + fun refreshQuizContent(project: Project) { + // Remove existing quiz cards and coding task cards but keep back button and fixed content val componentsToKeep = mutableListOf() for (i in 0 until content.componentCount) { val component = content.getComponent(i) - // Keep back button and headers, but remove quiz cards and no-quiz message - if (component !is QuizCard && !isNoQuizMessage(component)) { + // Keep back button and headers, but remove quiz cards, task cards, and status messages + if (component !is QuizCard && component !is TaskCard && + !isNoQuizMessage(component) && !isNoCodingTaskMessage(component) && + !isGeneratingMessage(component) && !isFallbackTaskHeader(component)) { componentsToKeep.add(component) } } content.removeAll() - // Re-add kept components (back button, headers, tasks) + // Re-add kept components (back button and main headers) componentsToKeep.forEach { content.add(it) } - // Clear theme children of removed quiz cards - themeChildren.removeAll { it is QuizCard } + // Clear theme children of removed cards + themeChildren.removeAll { it is QuizCard || it is TaskCard } // Check and reload quiz content val questions = quizService.getQuestions() @@ -157,11 +217,20 @@ class QuizPage( afterQuizComponents.forEach { content.remove(it) } // Add quiz questions loadQuizQuestions(questions) - // Re-add the practical tasks section + // Re-add the practical tasks section header afterQuizComponents.forEach { content.add(it) } + // Load coding tasks after the header + loadCodingTasks(project ) } else { // Fallback: just add questions at the end loadQuizQuestions(questions) + // Add practical tasks header and load coding tasks + val header = JLabel("Practical tasks").apply { + font = JBFont.label().asBold() + border = JBUI.Borders.emptyTop(16) + } + content.add(header) + loadCodingTasks(project) } } else { // Show no quiz message @@ -178,13 +247,22 @@ class QuizPage( component is JLabel && component.text.contains("Practical tasks") } if (headerIndex >= 0) { - content.remove(componentsToKeep[headerIndex]) + // Remove components from header onwards to re-add them after no-quiz message + val afterQuizComponents = componentsToKeep.drop(headerIndex) + afterQuizComponents.forEach { content.remove(it) } content.add(noQuizLabel) - content.add(componentsToKeep[headerIndex]) - // Re-add remaining components - componentsToKeep.drop(headerIndex + 1).forEach { content.add(it) } + afterQuizComponents.forEach { content.add(it) } + // Load coding tasks after the header + loadCodingTasks(project) } else { content.add(noQuizLabel) + // Add practical tasks header and load coding tasks + val header = JLabel("Practical tasks").apply { + font = JBFont.label().asBold() + border = JBUI.Borders.emptyTop(16) + } + content.add(header) + loadCodingTasks(project) } } @@ -195,4 +273,17 @@ class QuizPage( private fun isNoQuizMessage(component: java.awt.Component): Boolean { return component is JLabel && component.text.contains("No Quiz Available") } + + private fun isNoCodingTaskMessage(component: java.awt.Component): Boolean { + return component is JLabel && (component.text.contains("No Coding Tasks Available") || + component.text.contains("Coding tasks will be generated")) + } + + private fun isGeneratingMessage(component: java.awt.Component): Boolean { + return component is JLabel && component.text.contains("Generating Coding Tasks") + } + + private fun isFallbackTaskHeader(component: java.awt.Component): Boolean { + return component is JLabel && component.text.contains("Example Tasks (Static)") + } } diff --git a/src/main/kotlin/llm_pipeline/CodingTaskAction.kt b/src/main/kotlin/llm_pipeline/CodingTaskAction.kt new file mode 100644 index 0000000..302b232 --- /dev/null +++ b/src/main/kotlin/llm_pipeline/CodingTaskAction.kt @@ -0,0 +1,154 @@ +package llm_pipeline + +import com.gitdiff.CodingTaskList +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.* + +/** + * Action to generate coding tasks based on guide content using LLM analysis + */ +class CodingTaskAction : AnAction("Generate Coding Tasks") { + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + + ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Generating Coding Tasks", false) { + override fun run(indicator: ProgressIndicator) { + try { + indicator.text = "Checking for existing guide..." + + // Check if guide is already available in GuideService + val guideService = com.gitdiff.GuideService.getInstance(project) + val guide = guideService.getGuide() + + if (guide.content.isBlank()) { + indicator.text = "Generating guide for coding task content..." + + // Generate the guide first if not available + val guideSuccess = SemanticGuideGenerator.generateComprehensiveGuide(project) + + if (guideSuccess == null) { + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog(project, "Failed to generate guide. Please ensure you have a valid Git repository with recent commits.", "Coding Task Generation Error") + } + return + } + } + + indicator.text = "Creating practical coding tasks..." + + // Generate coding tasks based on the guide content stored in service + val codingTaskSuccess = CodingTaskGenerator.generateCodingTasksFromGuide(project) + + if (!codingTaskSuccess) { + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog(project, "Failed to generate coding tasks from guide content.", "Coding Task Generation Error") + } + return + } + + ApplicationManager.getApplication().invokeLater { + showCodingTaskDialog(project) + } + + } catch (e: Exception) { + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog(project, "Failed to generate coding tasks: ${e.message}", "Coding Task Generation Error") + } + } + } + }) + } + + private fun showCodingTaskDialog(project: com.intellij.openapi.project.Project) { + // Get coding tasks from CodingTaskService + val codingTaskService = com.gitdiff.CodingTaskService.getInstance(project) + val codingTasks = codingTaskService.getCodingTasks() + + // Format coding tasks for display + val formattedTasks = formatCodingTasksForDisplay(codingTasks) + + val dialog = CodingTaskDialog(formattedTasks) + dialog.show() + } + + /** + * Format the structured coding tasks for display in the dialog + */ + private fun formatCodingTasksForDisplay(codingTasks: CodingTaskList): String { + if (codingTasks.tasks.isEmpty()) { + return "No coding tasks available. Please generate a guide first." + } + + return buildString { + appendLine("# Practical Coding Tasks") + appendLine() + appendLine("Based on the code changes and concepts in your guide, here are practical programming exercises:") + appendLine() + + codingTasks.tasks.forEachIndexed { index, task -> + appendLine("## Task ${task.id}: ${task.title}") + appendLine() + appendLine("**Language:** ${task.languageId}") + appendLine() + appendLine("```${task.languageId}") + appendLine(task.initialCode) + appendLine("```") + appendLine() + + if (index < codingTasks.tasks.size - 1) { + appendLine("---") + appendLine() + } + } + } + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} + +/** + * Dialog to display generated coding tasks + */ +private class CodingTaskDialog(private val content: String) : DialogWrapper(true) { + + init { + title = "Generated Coding Tasks" + setOKButtonText("Close") + setCancelButtonText("null") + init() + } + + override fun createCenterPanel(): JComponent { + val textArea = JTextArea(content).apply { + isEditable = false + font = font.deriveFont(12f) + lineWrap = true + wrapStyleWord = true + background = UIManager.getColor("Panel.background") + } + + val scrollPane = JScrollPane(textArea).apply { + preferredSize = Dimension(800, 600) + verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED + horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED + } + + return JPanel(BorderLayout()).apply { + add(scrollPane, BorderLayout.CENTER) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/llm_pipeline/CodingTaskGenerator.kt b/src/main/kotlin/llm_pipeline/CodingTaskGenerator.kt new file mode 100644 index 0000000..86c7ee3 --- /dev/null +++ b/src/main/kotlin/llm_pipeline/CodingTaskGenerator.kt @@ -0,0 +1,311 @@ +package llm_pipeline + +import com.config.GlobalConfig +import com.gitdiff.* +import com.intellij.openapi.project.Project +import org.json.JSONObject +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest + +/** + * Generates coding tasks based on guide content using LLM analysis. + * Creates practical programming exercises related to the code changes in the guide. + */ +object CodingTaskGenerator { + + /** + * Generate coding tasks based on the guide content stored in GuideService + * Returns true if successful, false if failed. Stores result in CodingTaskService. + */ + fun generateCodingTasksFromGuide( + project: Project, + modelId: String = GlobalConfig.model_name, + maxTokens: Int = 2000 + ): Boolean { + // Get guide content from GuideService + val guideService = GuideService.getInstance(project) + val guide = guideService.getGuide() + val guideContent = guide.content + + if (guideContent.isBlank()) { + return false + } + + val prompt = buildCodingTaskPrompt(guideContent) + return try { + val raw = callLLMAndSave(project, prompt, "coding_tasks", modelId, maxTokens) + val outer = JSONObject(raw) + if (outer.has("error")) { + false + } else { + val content = outer + .getJSONArray("choices") + .getJSONObject(0) + .getJSONObject("message") + .getString("content") + val tasksContent = stripMarkdownFences(content) + + // Parse and store tasks in CodingTaskService + storeCodingTasksInService(project, tasksContent) + true + } + } catch (t: Throwable) { + false + } + } + + /** + * Store the generated coding tasks in CodingTaskService for later access + */ + private fun storeCodingTasksInService(project: Project, tasksContent: String) { + val codingTaskService = CodingTaskService.getInstance(project) + + // Parse the coding tasks content to extract structured tasks + val tasks = parseCodingTasksContent(tasksContent) + + val codingTaskList = CodingTaskList(tasks = tasks) + codingTaskService.setCodingTasks(codingTaskList) + } + + /** + * Parse generated coding tasks content into structured CodingTask objects + */ + private fun parseCodingTasksContent(content: String): List { + val tasks = mutableListOf() + val lines = content.lines() + + var currentTask: CodingTask? = null + var currentCode = StringBuilder() + var taskId = 1 + var inCodeBlock = false + var currentLanguage = "kotlin" + + for (line in lines) { + when { + line.startsWith("### Task ") -> { + // Save previous task if exists + currentTask?.let { task -> + val finalCode = currentCode.toString().trim() + // Ensure we have some code content, provide fallback if empty + val codeToUse = if (finalCode.isBlank()) { + generateFallbackCode(task.title, task.languageId) + } else { + finalCode + } + tasks.add(task.copy(initialCode = codeToUse)) + } + + // Start new task + val title = line.removePrefix("### Task $taskId: ").trim() + currentTask = CodingTask( + id = taskId, + title = title, + languageId = currentLanguage, + initialCode = "" + ) + currentCode.clear() + taskId++ + } + line.startsWith("**Language:**") -> { + currentLanguage = line.removePrefix("**Language:**").trim().lowercase() + currentTask = currentTask?.copy(languageId = currentLanguage) + } + line.startsWith("```") -> { + inCodeBlock = !inCodeBlock + if (line.length > 3) { + // Extract language from code fence + val lang = line.removePrefix("```").trim() + if (lang.isNotEmpty()) { + currentLanguage = lang.lowercase() + currentTask = currentTask?.copy(languageId = currentLanguage) + } + } + } + inCodeBlock -> { + currentCode.appendLine(line) + } + } + } + + // Add the last task + currentTask?.let { task -> + val finalCode = currentCode.toString().trim() + // Ensure we have some code content, provide fallback if empty + val codeToUse = if (finalCode.isBlank()) { + generateFallbackCode(task.title, task.languageId) + } else { + finalCode + } + tasks.add(task.copy(initialCode = codeToUse)) + } + + return tasks + } + + /** + * Generate a fallback code template if LLM doesn't provide proper code + */ + private fun generateFallbackCode(title: String, language: String): String { + return when (language.lowercase()) { + "kotlin" -> """ + // TODO: Implement $title + // Input: [describe your input parameters here] + // Output: [describe expected return value here] + + fun solve(): Unit { + // TODO: Add your function parameters + // TODO: Implement the solution for: $title + // TODO: Return appropriate value + } + """.trimIndent() + + "java" -> """ + // TODO: Implement $title + // Input: [describe your input parameters here] + // Output: [describe expected return value here] + + public class Solution { + public void solve() { + // TODO: Add your function parameters + // TODO: Implement the solution for: $title + // TODO: Return appropriate value + } + } + """.trimIndent() + + else -> """ + # TODO: Implement $title + # Input: [describe your input parameters here] + # Output: [describe expected return value here] + + def solve(): + # TODO: Add your function parameters + # TODO: Implement the solution for: $title + # TODO: Return appropriate value + pass + """.trimIndent() + } + } + + /** + * Build a prompt that generates practical coding tasks based on the guide content + */ + private fun buildCodingTaskPrompt(guideContent: String): String { + return buildString { + appendLine("Based on the comprehensive code guide provided below, create exactly 3 practical coding tasks.") + appendLine() + appendLine("CODING TASK REQUIREMENTS:") + appendLine("- Create hands-on programming exercises that relate to the code changes shown in the guide") + appendLine("- Each task MUST include a complete function template with clear input/output parameters") + appendLine("- Function signatures should be complete and compilable") + appendLine("- Include meaningful parameter names that indicate expected input") + appendLine("- Add return type that clearly shows expected output") + appendLine("- Include TODO comments explaining what to implement") + appendLine("- Focus on algorithmic thinking, design patterns, and practical implementation") + appendLine("- Use languages relevant to the codebase (prefer Kotlin, but Java/Python acceptable)") + appendLine("- Make tasks progressively challenging") + appendLine() + appendLine("REQUIRED FORMAT:") + appendLine("### Task 1: [Clear, descriptive title]") + appendLine("**Language:** [programming language]") + appendLine("**Description:** [Brief description of what to implement, including input/output explanation]") + appendLine("```[language]") + appendLine("// TODO: [Specific instruction about what the function should do]") + appendLine("// Input: [describe input parameters]") + appendLine("// Output: [describe expected return value]") + appendLine("") + appendLine("fun functionName(param1: Type, param2: Type): ReturnType {") + appendLine(" // TODO: [specific implementation steps]") + appendLine(" // TODO: [more specific steps]") + appendLine(" return defaultValue // TODO: replace with actual implementation") + appendLine("}") + appendLine("```") + appendLine() + appendLine("### Task 2: [Clear, descriptive title]") + appendLine("[... same format with complete function template ...]") + appendLine() + appendLine("### Task 3: [Clear, descriptive title]") + appendLine("[... same format with complete function template ...]") + appendLine() + appendLine("EXAMPLE GOOD TASK FORMAT:") + appendLine("```kotlin") + appendLine("// TODO: Implement BFS to find shortest path in a grid") + appendLine("// Input: grid (2D char array), start (Point), goal (Point)") + appendLine("// Output: shortest distance as Int, or -1 if unreachable") + appendLine("") + appendLine("data class Point(val row: Int, val col: Int)") + appendLine("") + appendLine("fun findShortestPath(grid: Array, start: Point, goal: Point): Int {") + appendLine(" // TODO: Create a queue for BFS and visited set") + appendLine(" // TODO: Add start point to queue with distance 0") + appendLine(" // TODO: While queue is not empty, process neighbors") + appendLine(" // TODO: Return distance when goal is found") + appendLine(" return -1 // TODO: replace with actual implementation") + appendLine("}") + appendLine("```") + appendLine() + appendLine("FOCUS AREAS:") + appendLine("- Data structures and algorithms from the codebase") + appendLine("- Design patterns and architectural concepts") + appendLine("- Code organization and best practices") + appendLine("- Problem-solving techniques demonstrated") + appendLine("- Practical implementation challenges") + appendLine() + appendLine("SEMANTIC HUNKS AND GUIDE CONTENT TO ANALYZE:") + appendLine("=".repeat(50)) + appendLine(guideContent) + appendLine("=".repeat(50)) + appendLine() + appendLine("Generate the coding tasks now using the EXACT format specified above with COMPLETE function templates:") + } + } + + /** + * Call LLM using the same approach as other generators + */ + private fun callLLMAndSave( + project: Project, + prompt: String, + filenameBase: String, + modelId: String = GlobalConfig.model_name, + maxTokens: Int = 2000 + ): String { + val quoted = JSONObject.quote(prompt) + val system = "You are an expert software engineering instructor that creates practical coding exercises based on code changes and software guides. Create educational programming tasks that help developers practice the concepts, patterns, and techniques shown in real codebases." + val systemQuoted = JSONObject.quote(system) + val requestBody = """ + { + "model": "$modelId", + "messages": [ + {"role": "system", "content": $systemQuoted}, + {"role": "user", "content": $quoted} + ], + "max_tokens": $maxTokens + } + """.trimIndent() + + val apiKey = com.settings.OpenRouterSettings.getInstance().apiKey ?: throw IllegalStateException("OPENROUTER_API_KEY not set") + val client = HttpClient.newHttpClient() + val request = HttpRequest.newBuilder() + .uri(URI.create("https://openrouter.ai/api/v1/chat/completions")) + .header("Authorization", "Bearer $apiKey") + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build() + + val response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()) + return response.body() + } + + /** + * Remove markdown code fences from the generated content + */ + private fun stripMarkdownFences(content: String): String { + return content + .replace("```markdown\n", "") + .replace("```\n", "") + .replace("```", "") + .trim() + } +} \ No newline at end of file