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 ConceptExplainer from './ConceptExplainer';
|
||||
import ScenarioQuiz from './ScenarioQuiz';
|
||||
import FlashcardSet from './FlashcardSet';
|
||||
import ReflectionPrompt from './ReflectionPrompt';
|
||||
import { useMicroLearningCompletions } from '../../hooks/useMicroLearningCompletions';
|
||||
|
||||
export default function MicroLearningContainer({ microLearning, sessionWeek, onCompletedSuccessfully }) {
|
||||
@@ -42,8 +41,6 @@ export default function MicroLearningContainer({ microLearning, sessionWeek, onC
|
||||
return <ScenarioQuiz {...props} />;
|
||||
case 'flashcard_set':
|
||||
return <FlashcardSet {...props} />;
|
||||
case 'reflection_prompt':
|
||||
return <ReflectionPrompt {...props} />;
|
||||
default:
|
||||
return <div>Unknown micro learning type.</div>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Loader, BookOpen, Target, Layers, MessageCircle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loader, BookOpen, Target, Layers, CheckCircle2 } from 'lucide-react';
|
||||
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
|
||||
import { getExistingTypesForTopic } from '../../lib/microLearningService';
|
||||
import MicroLearningContainer from './MicroLearningContainer';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
@@ -23,12 +24,6 @@ const TYPES = [
|
||||
description: 'Test your recall with a set of quick flashcards.',
|
||||
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 }) {
|
||||
@@ -36,6 +31,15 @@ export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCom
|
||||
const [selectedML, setSelectedML] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
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) => {
|
||||
setLoading(true);
|
||||
@@ -105,12 +109,26 @@ 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>
|
||||
</div>
|
||||
<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 }) => {
|
||||
const isAvailable = availableTypes.has(key);
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all group"
|
||||
className={`relative cursor-pointer border-2 rounded-[var(--r-md)] p-6 hover:bg-teal/5 transition-all group ${
|
||||
isAvailable
|
||||
? 'border-teal/40 bg-teal/[0.03] hover:border-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" />
|
||||
@@ -119,7 +137,8 @@ export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCom
|
||||
</div>
|
||||
<p className="text-sm text-fg-muted">{description}</p>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</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_SCENARIO_QUIZ_TOOL,
|
||||
EMIT_FLASHCARD_SET_TOOL,
|
||||
EMIT_REFLECTION_PROMPT_TOOL,
|
||||
} from './llmTools';
|
||||
import * as db from './db';
|
||||
|
||||
@@ -53,15 +52,6 @@ Mix three question types:
|
||||
- Relationships: "How does X relate to Y?"
|
||||
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.
|
||||
@@ -126,6 +116,23 @@ export async function regenerateMicroLearning(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).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user