feat: drop reflection_prompt type and flag cached micro-learnings
Remove the reflection_prompt micro-learning format end-to-end: type config, tool definition, container case, selector tile, and the ReflectionPrompt component file. The format wasn't pulling its weight as a learning surface. Add a Beschikbaar badge to selector tiles whose topic already has a published micro-learning of that type, so users know which formats open instantly instead of triggering a fresh generation. Cached records are fetched once per topic via the new getExistingTypesForTopic helper, and re-fetched after a generation returns so newly-created formats light up without a manual refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ConceptExplainer from './ConceptExplainer';
|
import ConceptExplainer from './ConceptExplainer';
|
||||||
import ScenarioQuiz from './ScenarioQuiz';
|
import ScenarioQuiz from './ScenarioQuiz';
|
||||||
import FlashcardSet from './FlashcardSet';
|
import FlashcardSet from './FlashcardSet';
|
||||||
import ReflectionPrompt from './ReflectionPrompt';
|
|
||||||
import { useMicroLearningCompletions } from '../../hooks/useMicroLearningCompletions';
|
import { useMicroLearningCompletions } from '../../hooks/useMicroLearningCompletions';
|
||||||
|
|
||||||
export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) {
|
export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) {
|
||||||
@@ -42,8 +41,6 @@ export default function MicroLearningContainer({ microLearning, sessionWeek, onC
|
|||||||
return <ScenarioQuiz {...props} />;
|
return <ScenarioQuiz {...props} />;
|
||||||
case 'flashcard_set':
|
case 'flashcard_set':
|
||||||
return <FlashcardSet {...props} />;
|
return <FlashcardSet {...props} />;
|
||||||
case 'reflection_prompt':
|
|
||||||
return <ReflectionPrompt {...props} />;
|
|
||||||
default:
|
default:
|
||||||
return <div>Unknown micro learning type.</div>;
|
return <div>Unknown micro learning type.</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Loader, BookOpen, Target, Layers, MessageCircle } from 'lucide-react';
|
import { Loader, BookOpen, Target, Layers, CheckCircle2 } from 'lucide-react';
|
||||||
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
|
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
|
||||||
|
import { getExistingTypesForTopic } from '../../lib/microLearningService';
|
||||||
import MicroLearningContainer from './MicroLearningContainer';
|
import MicroLearningContainer from './MicroLearningContainer';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
|
|
||||||
@@ -23,12 +24,6 @@ const TYPES = [
|
|||||||
description: 'Test your recall with a set of quick flashcards.',
|
description: 'Test your recall with a set of quick flashcards.',
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'reflection_prompt',
|
|
||||||
label: 'Reflection Prompt',
|
|
||||||
description: 'Connect the topic to your own professional experience.',
|
|
||||||
icon: MessageCircle,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCompleted }) {
|
export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCompleted }) {
|
||||||
@@ -36,6 +31,15 @@ export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCom
|
|||||||
const [selectedML, setSelectedML] = useState(null);
|
const [selectedML, setSelectedML] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [availableTypes, setAvailableTypes] = useState(() => new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
getExistingTypesForTopic(topicId).then((set) => {
|
||||||
|
if (!cancelled) setAvailableTypes(set);
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [topicId, selectedML]);
|
||||||
|
|
||||||
const handleSelection = async (type) => {
|
const handleSelection = async (type) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -105,21 +109,36 @@ export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCom
|
|||||||
<p className="text-sm text-fg-muted mt-1">Select how you want to engage with this topic.</p>
|
<p className="text-sm text-fg-muted mt-1">Select how you want to engage with this topic.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{TYPES.map(({ key, label, description, icon: Icon }) => (
|
{TYPES.map(({ key, label, description, icon: Icon }) => {
|
||||||
<div
|
const isAvailable = availableTypes.has(key);
|
||||||
key={key}
|
return (
|
||||||
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all group"
|
<div
|
||||||
onClick={() => handleSelection(key)}
|
key={key}
|
||||||
>
|
className={`relative cursor-pointer border-2 rounded-[var(--r-md)] p-6 hover:bg-teal/5 transition-all group ${
|
||||||
<div className="flex items-center gap-3 mb-3">
|
isAvailable
|
||||||
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0 group-hover:bg-teal/20 transition-colors">
|
? 'border-teal/40 bg-teal/[0.03] hover:border-teal'
|
||||||
<Icon size={20} className="text-teal" />
|
: 'border-bg-warm hover:border-teal'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelection(key)}
|
||||||
|
>
|
||||||
|
{isAvailable && (
|
||||||
|
<span
|
||||||
|
className="absolute top-3 right-3 flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wide text-teal bg-teal/10 px-2 py-0.5 rounded-full"
|
||||||
|
title="Eerder gegenereerde content beschikbaar — opent direct."
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={12} /> Beschikbaar
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0 group-hover:bg-teal/20 transition-colors">
|
||||||
|
<Icon size={20} className="text-teal" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg text-teal">{label}</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-lg text-teal">{label}</h3>
|
<p className="text-sm text-fg-muted">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-fg-muted">{description}</p>
|
);
|
||||||
</div>
|
})}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import Card from '../ui/Card';
|
|
||||||
import Button from '../ui/Button';
|
|
||||||
|
|
||||||
export default function ReflectionPrompt({ content, onComplete }) {
|
|
||||||
const [response, setResponse] = useState('');
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!response.trim() || submitted) return;
|
|
||||||
|
|
||||||
setSubmitted(true);
|
|
||||||
onComplete();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full">
|
|
||||||
<div className="mb-6 pb-4 border-b border-bg-warm">
|
|
||||||
<h2 className="text-xl font-bold">Reflection Prompt</h2>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="prose dark:prose-invert">
|
|
||||||
<p className="text-lg font-medium">{content?.prompt}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!submitted ? (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<textarea
|
|
||||||
className="w-full min-h-[150px] p-4 border rounded-[var(--r-sm)] resize-y focus:ring-2 focus:ring-teal focus:outline-none bg-bg"
|
|
||||||
placeholder="Write your reflection here..."
|
|
||||||
value={response}
|
|
||||||
onChange={(e) => setResponse(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button type="submit" disabled={!response.trim()}>Submit Reflection</Button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="p-4 bg-bg-warm rounded-[var(--r-sm)]">
|
|
||||||
<h4 className="text-sm font-semibold text-fg-muted uppercase tracking-wider mb-2">Your Reflection</h4>
|
|
||||||
<p className="whitespace-pre-wrap">{response}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-teal/5 border border-teal/20 rounded-[var(--r-sm)]">
|
|
||||||
<h4 className="text-sm font-semibold text-teal uppercase tracking-wider mb-2">Model Answer</h4>
|
|
||||||
<div className="prose dark:prose-invert">
|
|
||||||
<p>{content?.model_answer}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -418,15 +418,3 @@ export const EMIT_FLASHCARD_SET_TOOL = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMIT_REFLECTION_PROMPT_TOOL = {
|
|
||||||
name: 'emit_reflection_prompt',
|
|
||||||
description: 'Return an open-ended reflection question that asks the employee to connect the topic to their own professional experience, plus a model answer showing the expected depth and specificity.',
|
|
||||||
input_schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
prompt: { type: 'string', description: 'An open-ended question that cannot be answered with a fact. It must require the employee to think about their own context.' },
|
|
||||||
model_answer: { type: 'string', description: 'An example of a thoughtful, specific response (3–5 sentences). This is not a rubric — it illustrates depth.' },
|
|
||||||
},
|
|
||||||
required: ['prompt', 'model_answer'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
EMIT_CONCEPT_EXPLAINER_TOOL,
|
EMIT_CONCEPT_EXPLAINER_TOOL,
|
||||||
EMIT_SCENARIO_QUIZ_TOOL,
|
EMIT_SCENARIO_QUIZ_TOOL,
|
||||||
EMIT_FLASHCARD_SET_TOOL,
|
EMIT_FLASHCARD_SET_TOOL,
|
||||||
EMIT_REFLECTION_PROMPT_TOOL,
|
|
||||||
} from './llmTools';
|
} from './llmTools';
|
||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
|
|
||||||
@@ -53,15 +52,6 @@ Mix three question types:
|
|||||||
- Relationships: "How does X relate to Y?"
|
- Relationships: "How does X relate to Y?"
|
||||||
Keep answers concise — one or two sentences maximum.`,
|
Keep answers concise — one or two sentences maximum.`,
|
||||||
},
|
},
|
||||||
reflection_prompt: {
|
|
||||||
tool: EMIT_REFLECTION_PROMPT_TOOL,
|
|
||||||
tier: 'fast',
|
|
||||||
maxTokens: 1024,
|
|
||||||
instructions: `Generate a reflection prompt.
|
|
||||||
The question must be open-ended and cannot be answered with a fact.
|
|
||||||
It must require the employee to think about their own professional context — their team, their role, their past experience.
|
|
||||||
The model answer should show depth and specificity (3–5 sentences). It is not a rubric — it is an example of thoughtful reflection.`,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `You are an expert learning content writer for Respellion, an internal IT company.
|
const SYSTEM_PROMPT = `You are an expert learning content writer for Respellion, an internal IT company.
|
||||||
@@ -126,6 +116,23 @@ export async function regenerateMicroLearning(topicId, type) {
|
|||||||
return getOrGenerateMicroLearning(topicId, type);
|
return getOrGenerateMicroLearning(topicId, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the set of micro-learning types that already have a published
|
||||||
|
* record for the given topic. Used by the selector UI to flag formats
|
||||||
|
* that are ready to read instantly (no generation latency).
|
||||||
|
*/
|
||||||
|
export async function getExistingTypesForTopic(topicId) {
|
||||||
|
try {
|
||||||
|
const records = await pb.collection('micro_learnings').getFullList({
|
||||||
|
filter: `topic_id = "${topicId}" && status = "published"`,
|
||||||
|
fields: 'type',
|
||||||
|
});
|
||||||
|
return new Set(records.map((r) => r.type));
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all cached micro learnings for a topic (all types).
|
* Delete all cached micro learnings for a topic (all types).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user