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
702 lines
19 KiB
Go
702 lines
19 KiB
Go
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
|
|
}
|