feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
This commit is contained in:
701
backend/internal/learning/stepper.go
Normal file
701
backend/internal/learning/stepper.go
Normal file
@@ -0,0 +1,701 @@
|
||||
package learning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gooseek/backend/internal/llm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LearningMode string
|
||||
|
||||
const (
|
||||
ModeExplain LearningMode = "explain"
|
||||
ModeGuided LearningMode = "guided"
|
||||
ModeInteractive LearningMode = "interactive"
|
||||
ModePractice LearningMode = "practice"
|
||||
ModeQuiz LearningMode = "quiz"
|
||||
)
|
||||
|
||||
type DifficultyLevel string
|
||||
|
||||
const (
|
||||
DifficultyBeginner DifficultyLevel = "beginner"
|
||||
DifficultyIntermediate DifficultyLevel = "intermediate"
|
||||
DifficultyAdvanced DifficultyLevel = "advanced"
|
||||
DifficultyExpert DifficultyLevel = "expert"
|
||||
)
|
||||
|
||||
type StepByStepLesson struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Topic string `json:"topic"`
|
||||
Difficulty DifficultyLevel `json:"difficulty"`
|
||||
Mode LearningMode `json:"mode"`
|
||||
Steps []LearningStep `json:"steps"`
|
||||
Prerequisites []string `json:"prerequisites,omitempty"`
|
||||
LearningGoals []string `json:"learningGoals"`
|
||||
EstimatedTime int `json:"estimatedTimeMinutes"`
|
||||
Progress LessonProgress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type LearningStep struct {
|
||||
ID string `json:"id"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Type StepType `json:"type"`
|
||||
Content StepContent `json:"content"`
|
||||
Interaction *StepInteraction `json:"interaction,omitempty"`
|
||||
Hints []string `json:"hints,omitempty"`
|
||||
Examples []Example `json:"examples,omitempty"`
|
||||
Practice *PracticeExercise `json:"practice,omitempty"`
|
||||
Quiz *QuizQuestion `json:"quiz,omitempty"`
|
||||
Duration int `json:"durationSeconds,omitempty"`
|
||||
Status StepStatus `json:"status"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type StepType string
|
||||
|
||||
const (
|
||||
StepExplanation StepType = "explanation"
|
||||
StepVisualization StepType = "visualization"
|
||||
StepCode StepType = "code"
|
||||
StepInteractive StepType = "interactive"
|
||||
StepPractice StepType = "practice"
|
||||
StepQuiz StepType = "quiz"
|
||||
StepSummary StepType = "summary"
|
||||
StepCheckpoint StepType = "checkpoint"
|
||||
)
|
||||
|
||||
type StepStatus string
|
||||
|
||||
const (
|
||||
StatusLocked StepStatus = "locked"
|
||||
StatusAvailable StepStatus = "available"
|
||||
StatusInProgress StepStatus = "in_progress"
|
||||
StatusCompleted StepStatus = "completed"
|
||||
StatusSkipped StepStatus = "skipped"
|
||||
)
|
||||
|
||||
type StepContent struct {
|
||||
Text string `json:"text"`
|
||||
Markdown string `json:"markdown,omitempty"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
Code *CodeContent `json:"code,omitempty"`
|
||||
Visualization *VisualizationContent `json:"visualization,omitempty"`
|
||||
Media *MediaContent `json:"media,omitempty"`
|
||||
Formula string `json:"formula,omitempty"`
|
||||
Highlights []TextHighlight `json:"highlights,omitempty"`
|
||||
}
|
||||
|
||||
type CodeContent struct {
|
||||
Language string `json:"language"`
|
||||
Code string `json:"code"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Runnable bool `json:"runnable"`
|
||||
Editable bool `json:"editable"`
|
||||
Highlights []int `json:"highlights,omitempty"`
|
||||
Annotations []CodeAnnotation `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
type CodeAnnotation struct {
|
||||
Line int `json:"line"`
|
||||
Text string `json:"text"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type VisualizationContent struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
type MediaContent struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
}
|
||||
|
||||
type TextHighlight struct {
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
Text string `json:"text"`
|
||||
Type string `json:"type"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type StepInteraction struct {
|
||||
Type string `json:"type"`
|
||||
Prompt string `json:"prompt"`
|
||||
Options []Option `json:"options,omitempty"`
|
||||
Validation *Validation `json:"validation,omitempty"`
|
||||
Feedback *Feedback `json:"feedback,omitempty"`
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
IsCorrect bool `json:"isCorrect,omitempty"`
|
||||
Feedback string `json:"feedback,omitempty"`
|
||||
}
|
||||
|
||||
type Validation struct {
|
||||
Type string `json:"type"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Expected string `json:"expected,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
Correct string `json:"correct"`
|
||||
Incorrect string `json:"incorrect"`
|
||||
Partial string `json:"partial,omitempty"`
|
||||
}
|
||||
|
||||
type Example struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Input string `json:"input,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
type PracticeExercise struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Instructions string `json:"instructions"`
|
||||
Starter string `json:"starter,omitempty"`
|
||||
Solution string `json:"solution,omitempty"`
|
||||
TestCases []TestCase `json:"testCases,omitempty"`
|
||||
Hints []string `json:"hints,omitempty"`
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
Input string `json:"input"`
|
||||
Expected string `json:"expected"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
}
|
||||
|
||||
type QuizQuestion struct {
|
||||
Question string `json:"question"`
|
||||
Type string `json:"type"`
|
||||
Options []Option `json:"options,omitempty"`
|
||||
CorrectIndex []int `json:"correctIndex,omitempty"`
|
||||
Explanation string `json:"explanation"`
|
||||
Points int `json:"points"`
|
||||
}
|
||||
|
||||
type LessonProgress struct {
|
||||
CurrentStep int `json:"currentStep"`
|
||||
CompletedSteps []int `json:"completedSteps"`
|
||||
Score int `json:"score"`
|
||||
MaxScore int `json:"maxScore"`
|
||||
TimeSpent int `json:"timeSpentSeconds"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
LastAccessed time.Time `json:"lastAccessed"`
|
||||
Completed bool `json:"completed"`
|
||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type LearningGenerator struct {
|
||||
llm llm.Client
|
||||
}
|
||||
|
||||
func NewLearningGenerator(llmClient llm.Client) *LearningGenerator {
|
||||
return &LearningGenerator{llm: llmClient}
|
||||
}
|
||||
|
||||
type GenerateLessonOptions struct {
|
||||
Topic string
|
||||
Query string
|
||||
Difficulty DifficultyLevel
|
||||
Mode LearningMode
|
||||
MaxSteps int
|
||||
Locale string
|
||||
IncludeCode bool
|
||||
IncludeQuiz bool
|
||||
}
|
||||
|
||||
func (g *LearningGenerator) GenerateLesson(ctx context.Context, opts GenerateLessonOptions) (*StepByStepLesson, error) {
|
||||
if opts.MaxSteps == 0 {
|
||||
opts.MaxSteps = 10
|
||||
}
|
||||
if opts.Difficulty == "" {
|
||||
opts.Difficulty = DifficultyBeginner
|
||||
}
|
||||
if opts.Mode == "" {
|
||||
opts.Mode = ModeExplain
|
||||
}
|
||||
|
||||
langInstruction := ""
|
||||
if opts.Locale == "ru" {
|
||||
langInstruction = "Generate all content in Russian language."
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Create a step-by-step educational lesson on the following topic.
|
||||
|
||||
Topic: %s
|
||||
Query: %s
|
||||
Difficulty: %s
|
||||
Mode: %s
|
||||
Max Steps: %d
|
||||
Include Code Examples: %v
|
||||
Include Quiz: %v
|
||||
%s
|
||||
|
||||
Generate a structured lesson with these requirements:
|
||||
1. Break down the concept into clear, digestible steps
|
||||
2. Each step should build on the previous one
|
||||
3. Include explanations, examples, and visualizations where helpful
|
||||
4. For code topics, include runnable code snippets
|
||||
5. Add practice exercises for interactive learning
|
||||
6. Include quiz questions to test understanding
|
||||
|
||||
Respond in this JSON format:
|
||||
{
|
||||
"title": "Lesson title",
|
||||
"description": "Brief description",
|
||||
"learningGoals": ["Goal 1", "Goal 2"],
|
||||
"estimatedTimeMinutes": 15,
|
||||
"steps": [
|
||||
{
|
||||
"title": "Step title",
|
||||
"type": "explanation|code|interactive|practice|quiz|summary",
|
||||
"content": {
|
||||
"text": "Main explanation",
|
||||
"markdown": "## Formatted content",
|
||||
"code": {"language": "python", "code": "example", "runnable": true},
|
||||
"formula": "optional LaTeX formula"
|
||||
},
|
||||
"hints": ["Hint 1"],
|
||||
"examples": [{"title": "Example", "description": "...", "code": "..."}],
|
||||
"quiz": {
|
||||
"question": "...",
|
||||
"type": "multiple_choice",
|
||||
"options": [{"id": "a", "text": "Option A", "isCorrect": false}],
|
||||
"explanation": "..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}`, opts.Topic, opts.Query, opts.Difficulty, opts.Mode, opts.MaxSteps, opts.IncludeCode, opts.IncludeQuiz, langInstruction)
|
||||
|
||||
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
|
||||
Messages: []llm.Message{{Role: "user", Content: prompt}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonStr := extractJSON(result)
|
||||
|
||||
var parsed struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
LearningGoals []string `json:"learningGoals"`
|
||||
EstimatedTimeMinutes int `json:"estimatedTimeMinutes"`
|
||||
Steps []struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
Markdown string `json:"markdown"`
|
||||
Code *struct {
|
||||
Language string `json:"language"`
|
||||
Code string `json:"code"`
|
||||
Runnable bool `json:"runnable"`
|
||||
} `json:"code"`
|
||||
Formula string `json:"formula"`
|
||||
} `json:"content"`
|
||||
Hints []string `json:"hints"`
|
||||
Examples []struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Code string `json:"code"`
|
||||
} `json:"examples"`
|
||||
Quiz *struct {
|
||||
Question string `json:"question"`
|
||||
Type string `json:"type"`
|
||||
Options []struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
IsCorrect bool `json:"isCorrect"`
|
||||
} `json:"options"`
|
||||
Explanation string `json:"explanation"`
|
||||
} `json:"quiz"`
|
||||
} `json:"steps"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
|
||||
return g.createDefaultLesson(opts)
|
||||
}
|
||||
|
||||
lesson := &StepByStepLesson{
|
||||
ID: uuid.New().String(),
|
||||
Title: parsed.Title,
|
||||
Description: parsed.Description,
|
||||
Topic: opts.Topic,
|
||||
Difficulty: opts.Difficulty,
|
||||
Mode: opts.Mode,
|
||||
LearningGoals: parsed.LearningGoals,
|
||||
EstimatedTime: parsed.EstimatedTimeMinutes,
|
||||
Steps: make([]LearningStep, 0),
|
||||
Progress: LessonProgress{
|
||||
CurrentStep: 0,
|
||||
CompletedSteps: []int{},
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
for i, s := range parsed.Steps {
|
||||
step := LearningStep{
|
||||
ID: uuid.New().String(),
|
||||
Number: i + 1,
|
||||
Title: s.Title,
|
||||
Type: StepType(s.Type),
|
||||
Content: StepContent{
|
||||
Text: s.Content.Text,
|
||||
Markdown: s.Content.Markdown,
|
||||
Formula: s.Content.Formula,
|
||||
},
|
||||
Hints: s.Hints,
|
||||
Status: StatusAvailable,
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
step.Status = StatusLocked
|
||||
}
|
||||
|
||||
if s.Content.Code != nil {
|
||||
step.Content.Code = &CodeContent{
|
||||
Language: s.Content.Code.Language,
|
||||
Code: s.Content.Code.Code,
|
||||
Runnable: s.Content.Code.Runnable,
|
||||
Editable: true,
|
||||
}
|
||||
}
|
||||
|
||||
for _, ex := range s.Examples {
|
||||
step.Examples = append(step.Examples, Example{
|
||||
Title: ex.Title,
|
||||
Description: ex.Description,
|
||||
Code: ex.Code,
|
||||
})
|
||||
}
|
||||
|
||||
if s.Quiz != nil {
|
||||
quiz := &QuizQuestion{
|
||||
Question: s.Quiz.Question,
|
||||
Type: s.Quiz.Type,
|
||||
Explanation: s.Quiz.Explanation,
|
||||
Points: 10,
|
||||
}
|
||||
for _, opt := range s.Quiz.Options {
|
||||
quiz.Options = append(quiz.Options, Option{
|
||||
ID: opt.ID,
|
||||
Text: opt.Text,
|
||||
IsCorrect: opt.IsCorrect,
|
||||
})
|
||||
}
|
||||
step.Quiz = quiz
|
||||
}
|
||||
|
||||
lesson.Steps = append(lesson.Steps, step)
|
||||
}
|
||||
|
||||
lesson.Progress.MaxScore = len(lesson.Steps) * 10
|
||||
|
||||
return lesson, nil
|
||||
}
|
||||
|
||||
func (g *LearningGenerator) createDefaultLesson(opts GenerateLessonOptions) (*StepByStepLesson, error) {
|
||||
return &StepByStepLesson{
|
||||
ID: uuid.New().String(),
|
||||
Title: fmt.Sprintf("Learn: %s", opts.Topic),
|
||||
Description: opts.Query,
|
||||
Topic: opts.Topic,
|
||||
Difficulty: opts.Difficulty,
|
||||
Mode: opts.Mode,
|
||||
LearningGoals: []string{"Understand the basics"},
|
||||
EstimatedTime: 10,
|
||||
Steps: []LearningStep{
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
Number: 1,
|
||||
Title: "Introduction",
|
||||
Type: StepExplanation,
|
||||
Content: StepContent{
|
||||
Text: opts.Query,
|
||||
Markdown: fmt.Sprintf("# %s\n\n%s", opts.Topic, opts.Query),
|
||||
},
|
||||
Status: StatusAvailable,
|
||||
},
|
||||
},
|
||||
Progress: LessonProgress{
|
||||
CurrentStep: 0,
|
||||
CompletedSteps: []int{},
|
||||
MaxScore: 10,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *LearningGenerator) GenerateExplanation(ctx context.Context, topic string, difficulty DifficultyLevel, locale string) (*LearningStep, error) {
|
||||
langInstruction := ""
|
||||
if locale == "ru" {
|
||||
langInstruction = "Respond in Russian."
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Explain this topic step by step for a %s level learner.
|
||||
Topic: %s
|
||||
%s
|
||||
|
||||
Format your response with clear sections:
|
||||
1. Start with a simple definition
|
||||
2. Explain key concepts
|
||||
3. Provide a real-world analogy
|
||||
4. Give a concrete example
|
||||
|
||||
Use markdown formatting.`, difficulty, topic, langInstruction)
|
||||
|
||||
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
|
||||
Messages: []llm.Message{{Role: "user", Content: prompt}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LearningStep{
|
||||
ID: uuid.New().String(),
|
||||
Number: 1,
|
||||
Title: topic,
|
||||
Type: StepExplanation,
|
||||
Content: StepContent{
|
||||
Markdown: result,
|
||||
},
|
||||
Status: StatusAvailable,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *LearningGenerator) GenerateQuiz(ctx context.Context, topic string, numQuestions int, difficulty DifficultyLevel, locale string) ([]QuizQuestion, error) {
|
||||
langInstruction := ""
|
||||
if locale == "ru" {
|
||||
langInstruction = "Generate all questions and answers in Russian."
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Generate %d multiple choice quiz questions about: %s
|
||||
Difficulty level: %s
|
||||
%s
|
||||
|
||||
Respond in JSON format:
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Question text",
|
||||
"options": [
|
||||
{"id": "a", "text": "Option A", "isCorrect": false},
|
||||
{"id": "b", "text": "Option B", "isCorrect": true},
|
||||
{"id": "c", "text": "Option C", "isCorrect": false},
|
||||
{"id": "d", "text": "Option D", "isCorrect": false}
|
||||
],
|
||||
"explanation": "Why the correct answer is correct"
|
||||
}
|
||||
]
|
||||
}`, numQuestions, topic, difficulty, langInstruction)
|
||||
|
||||
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
|
||||
Messages: []llm.Message{{Role: "user", Content: prompt}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonStr := extractJSON(result)
|
||||
|
||||
var parsed struct {
|
||||
Questions []struct {
|
||||
Question string `json:"question"`
|
||||
Options []struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
IsCorrect bool `json:"isCorrect"`
|
||||
} `json:"options"`
|
||||
Explanation string `json:"explanation"`
|
||||
} `json:"questions"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
questions := make([]QuizQuestion, 0)
|
||||
for _, q := range parsed.Questions {
|
||||
quiz := QuizQuestion{
|
||||
Question: q.Question,
|
||||
Type: "multiple_choice",
|
||||
Explanation: q.Explanation,
|
||||
Points: 10,
|
||||
}
|
||||
for _, opt := range q.Options {
|
||||
quiz.Options = append(quiz.Options, Option{
|
||||
ID: opt.ID,
|
||||
Text: opt.Text,
|
||||
IsCorrect: opt.IsCorrect,
|
||||
})
|
||||
}
|
||||
questions = append(questions, quiz)
|
||||
}
|
||||
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
func (g *LearningGenerator) GeneratePracticeExercise(ctx context.Context, topic, language string, difficulty DifficultyLevel, locale string) (*PracticeExercise, error) {
|
||||
langInstruction := ""
|
||||
if locale == "ru" {
|
||||
langInstruction = "Write instructions and explanations in Russian."
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Create a coding practice exercise for: %s
|
||||
Programming language: %s
|
||||
Difficulty: %s
|
||||
%s
|
||||
|
||||
Generate:
|
||||
1. A clear problem statement
|
||||
2. Step-by-step instructions
|
||||
3. Starter code template
|
||||
4. Solution code
|
||||
5. Test cases
|
||||
|
||||
Respond in JSON:
|
||||
{
|
||||
"prompt": "Problem statement",
|
||||
"instructions": "Step-by-step instructions",
|
||||
"starter": "// Starter code",
|
||||
"solution": "// Solution code",
|
||||
"testCases": [
|
||||
{"input": "test input", "expected": "expected output"}
|
||||
],
|
||||
"hints": ["Hint 1", "Hint 2"]
|
||||
}`, topic, language, difficulty, langInstruction)
|
||||
|
||||
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
|
||||
Messages: []llm.Message{{Role: "user", Content: prompt}},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonStr := extractJSON(result)
|
||||
|
||||
var exercise PracticeExercise
|
||||
if err := json.Unmarshal([]byte(jsonStr), &exercise); err != nil {
|
||||
return &PracticeExercise{
|
||||
Prompt: topic,
|
||||
Instructions: "Practice this concept",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &exercise, nil
|
||||
}
|
||||
|
||||
func (l *StepByStepLesson) CompleteStep(stepIndex int) {
|
||||
if stepIndex < 0 || stepIndex >= len(l.Steps) {
|
||||
return
|
||||
}
|
||||
|
||||
l.Steps[stepIndex].Status = StatusCompleted
|
||||
|
||||
alreadyCompleted := false
|
||||
for _, idx := range l.Progress.CompletedSteps {
|
||||
if idx == stepIndex {
|
||||
alreadyCompleted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyCompleted {
|
||||
l.Progress.CompletedSteps = append(l.Progress.CompletedSteps, stepIndex)
|
||||
l.Progress.Score += 10
|
||||
}
|
||||
|
||||
if stepIndex+1 < len(l.Steps) {
|
||||
l.Steps[stepIndex+1].Status = StatusAvailable
|
||||
l.Progress.CurrentStep = stepIndex + 1
|
||||
}
|
||||
|
||||
if len(l.Progress.CompletedSteps) == len(l.Steps) {
|
||||
l.Progress.Completed = true
|
||||
now := time.Now()
|
||||
l.Progress.CompletedAt = &now
|
||||
}
|
||||
|
||||
l.UpdatedAt = time.Now()
|
||||
l.Progress.LastAccessed = time.Now()
|
||||
}
|
||||
|
||||
func (l *StepByStepLesson) SubmitQuizAnswer(stepIndex int, selectedOptions []string) (bool, string) {
|
||||
if stepIndex < 0 || stepIndex >= len(l.Steps) {
|
||||
return false, "Invalid step"
|
||||
}
|
||||
|
||||
step := &l.Steps[stepIndex]
|
||||
if step.Quiz == nil {
|
||||
return false, "No quiz in this step"
|
||||
}
|
||||
|
||||
correctCount := 0
|
||||
totalCorrect := 0
|
||||
|
||||
for _, opt := range step.Quiz.Options {
|
||||
if opt.IsCorrect {
|
||||
totalCorrect++
|
||||
}
|
||||
}
|
||||
|
||||
for _, selected := range selectedOptions {
|
||||
for _, opt := range step.Quiz.Options {
|
||||
if opt.ID == selected && opt.IsCorrect {
|
||||
correctCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isCorrect := correctCount == totalCorrect && len(selectedOptions) == totalCorrect
|
||||
|
||||
if isCorrect {
|
||||
return true, step.Quiz.Explanation
|
||||
}
|
||||
|
||||
return false, step.Quiz.Explanation
|
||||
}
|
||||
|
||||
func extractJSON(text string) string {
|
||||
re := regexp.MustCompile(`(?s)\{.*\}`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
return match
|
||||
}
|
||||
return "{}"
|
||||
}
|
||||
|
||||
func (l *StepByStepLesson) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(l)
|
||||
}
|
||||
|
||||
func ParseLesson(data []byte) (*StepByStepLesson, error) {
|
||||
var lesson StepByStepLesson
|
||||
if err := json.Unmarshal(data, &lesson); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &lesson, nil
|
||||
}
|
||||
Reference in New Issue
Block a user