Skip to content

Commit c865ded

Browse files
committed
feat: add support for flashcards, translations, and document search (Gemini)
1 parent 1a386c4 commit c865ded

6 files changed

Lines changed: 140 additions & 17 deletions

File tree

backend/agent.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,40 @@
247247
},
248248
"description": "Relevant educational YouTube video URLs",
249249
},
250+
"learning_objectives": {
251+
"type": "array",
252+
"items": {"type": "string"},
253+
"description": "3-5 learning objectives the block covers",
254+
},
255+
"flashcards": {
256+
"type": "array",
257+
"items": {
258+
"type": "object",
259+
"properties": {
260+
"front": {"type": "string"},
261+
"back": {"type": "string"},
262+
"variant": {
263+
"type": "string",
264+
"enum": ["vocabulary", "terminology", "concept", "formula", "definition"],
265+
},
266+
},
267+
"required": ["front", "back"],
268+
},
269+
"description": "Flashcards for spaced repetition study. Each card has a front (question/term) and back (answer/definition).",
270+
},
271+
"translations": {
272+
"type": "array",
273+
"items": {
274+
"type": "object",
275+
"properties": {
276+
"original": {"type": "string"},
277+
"translated": {"type": "string"},
278+
"language": {"type": "string"},
279+
},
280+
"required": ["original", "translated"],
281+
},
282+
"description": "Side-by-side translations of key terms, phrases, or passages. Include the target language name.",
283+
},
250284
},
251285
"required": ["content"],
252286
},
@@ -1142,7 +1176,7 @@ def execute_tool(name, arguments, session=None):
11421176
"content": content,
11431177
}
11441178
# Pass through structured fields if provided.
1145-
for field_name in ("learning_objectives", "key_terms", "references", "quiz_questions", "youtube_urls"):
1179+
for field_name in ("learning_objectives", "key_terms", "references", "quiz_questions", "youtube_urls", "flashcards", "translations"):
11461180
val = arguments.get(field_name)
11471181
if isinstance(val, list) and val:
11481182
result[field_name] = val
@@ -1334,6 +1368,31 @@ def _build_content_nodes(created_main_block, tool_results=None):
13341368
if questions:
13351369
nodes.append({"type": "quiz", "questions": questions})
13361370

1371+
fc = created_main_block.get("flashcards")
1372+
if isinstance(fc, list) and fc:
1373+
cards = []
1374+
for i, card in enumerate(fc):
1375+
if isinstance(card, dict) and card.get("front") and card.get("back"):
1376+
cards.append({
1377+
"id": f"fc-{i + 1}",
1378+
"front": str(card["front"]),
1379+
"back": str(card["back"]),
1380+
"variant": str(card.get("variant") or "concept"),
1381+
})
1382+
if cards:
1383+
nodes.append({"type": "flashcards", "cards": cards})
1384+
1385+
translations = created_main_block.get("translations")
1386+
if isinstance(translations, list) and translations:
1387+
for i, t in enumerate(translations):
1388+
if isinstance(t, dict) and t.get("original") and t.get("translated"):
1389+
nodes.append({
1390+
"type": "translation",
1391+
"original": str(t["original"]),
1392+
"translated": str(t["translated"]),
1393+
"language": str(t.get("language") or ""),
1394+
})
1395+
13371396
yt = created_main_block.get("youtube_urls")
13381397
if isinstance(yt, list) and yt:
13391398
for y in yt:

backend/lambda_handler.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6055,14 +6055,40 @@ def process_async_job(event):
60556055
requested_content_types = event.get("requested_content_types") or []
60566056
owner_user_id = str(event.get("owner_user_id") or "").strip()
60576057

6058-
# Prepend content-type requests to user message so the agent knows what to include.
6058+
# Prepend content-type requests with explicit format guidance per type.
6059+
_CONTENT_TYPE_FORMAT_HINTS = {
6060+
"quiz": "quiz -> use `create_main_block` with `quiz_questions` array (multiple_choice or true_false)",
6061+
"flashcards": "flashcards -> use `create_main_block` with `flashcards` array ({front, back, variant})",
6062+
"images": "images -> call `search_brave_images` or `search_wiki_images` inline",
6063+
"animation": "animation -> call `generate_math_animation` for math/physics/CS concepts",
6064+
"youtube": "youtube embed -> use `create_main_block` with `youtube_urls` array",
6065+
"tables": "tables -> include markdown tables (| col1 | col2 |) in your content",
6066+
"graphs": "graphs/charts -> include a markdown table with quantitative data AND append a chart tag on the next line (<<bar>>, <<pie>>, <<line>>, <<scatter>>, <<area>>, etc.)",
6067+
"code": "code -> include fenced code blocks with language tags (```python, ```javascript, etc.)",
6068+
"equations": "equations -> include LaTeX math: $inline$ or $$display$$ blocks",
6069+
"quote": "quote -> include blockquotes using > prefix",
6070+
"definition": "definition -> use blockquote format: > **Definition: Term** — explanation",
6071+
"translation": "translation -> use `create_main_block` with `translations` array ({original, translated, language})",
6072+
}
60596073
if requested_content_types and isinstance(requested_content_types, list):
6060-
types_str = ", ".join(str(t) for t in requested_content_types)
6061-
user_message = (
6062-
f"[IMPORTANT — The user has specifically requested the following content types be included "
6063-
f"in your response: {types_str}. You MUST include these content types in your answer. "
6064-
f"Use the appropriate tools and formatting for each requested type.]\n\n{user_message}"
6074+
hints = []
6075+
plain_names = []
6076+
for t in requested_content_types:
6077+
t_str = str(t).strip()
6078+
if not t_str:
6079+
continue
6080+
plain_names.append(t_str)
6081+
hint = _CONTENT_TYPE_FORMAT_HINTS.get(t_str)
6082+
if hint:
6083+
hints.append(f" - {hint}")
6084+
types_str = ", ".join(plain_names)
6085+
format_block = "\n".join(hints) if hints else ""
6086+
instruction = (
6087+
f"[IMPORTANT — The user has specifically requested the following content types: {types_str}.\n"
6088+
f"You MUST include these in your answer. Format guidance:\n"
6089+
f"{format_block}]\n\n"
60656090
)
6091+
user_message = instruction + user_message
60666092

60676093
started_at = time.time()
60686094
print(f"Processing job {job_id} for session {session_id}")

backend/tutor_prompts.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ def get_threaded_response_system_prompt(
202202
"- Use a search query specific to the user's follow-up question or the concept being explained.",
203203
"- Do NOT include images in a concluding section.",
204204
"",
205+
"Structured-field mapping (when user requests specific content types, use `create_main_block`):",
206+
"- quiz -> `quiz_questions`",
207+
"- flashcards -> `flashcards` (array of {front, back, variant})",
208+
"- translation -> `translations` (array of {original, translated, language})",
209+
"- key terms -> `key_terms`",
210+
"- learning objectives -> `learning_objectives`",
211+
"- references -> `references`",
212+
"- youtube/video resources -> `youtube_urls`",
213+
"",
205214
f"Parent block topic: {safe_topic}",
206215
"",
207216
"Full parent block content (use this context):",
@@ -242,6 +251,8 @@ def get_component_edit_system_prompt(
242251
"",
243252
"Structured-field mapping guidance:",
244253
"- quiz -> `quiz_questions`",
254+
"- flashcards -> `flashcards` (array of {front, back, variant})",
255+
"- translation -> `translations` (array of {original, translated, language})",
245256
"- key terms -> `key_terms`",
246257
"- learning objectives -> `learning_objectives`",
247258
"- references -> `references`",
@@ -319,7 +330,7 @@ def get_component_edit_system_prompt(
319330
- The `create_main_block` tool is the primary path for delivering final teaching content.
320331
- For simple queries (weather, basic math), DO NOT call this tool. Just answer them directly.
321332
- For educational queries, use your best judgment given the user's profile to call `create_main_block` with rich textbook-style markdown content.
322-
- **CRITICAL**: If the user explicitly asks for quizzes, flashcards, key terms, or learning objectives (even as a follow-up), you MUST use the `create_main_block` tool and populate the corresponding structured fields. Do NOT output raw quiz text in your direct response.
333+
- **CRITICAL**: If the user explicitly asks for quizzes, flashcards, translations, key terms, or learning objectives (even as a follow-up), you MUST use the `create_main_block` tool and populate the corresponding structured fields (`quiz_questions`, `flashcards`, `translations`, `key_terms`, `learning_objectives`). Do NOT output raw quiz or flashcard text in your direct response.
323334
324335
**ADAPTIVE PROFILE - IMPORTANT:**
325336
You have access to the `update_learner_profile` tool. Use it to dynamically adjust the user's profile when you observe:

frontend/src/components/app/ChatInput.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { usePreparedAttachments } from "@/hooks/usePreparedAttachments"
1717
import { useAuth } from "@/context/AuthContext"
1818
import ContentTypePicker, { ContentTypeChips } from "@/components/ui/ContentTypePicker"
1919
import { readStoredModel, writeStoredModel } from "@/lib/models"
20-
import { readStoredContentTypes, writeStoredContentTypes } from "@/lib/contentTypes"
20+
import { readStoredContentTypes, writeStoredContentTypes, buildContentTypeAutoPrompt } from "@/lib/contentTypes"
2121

2222
/* ═══════════════════════════════════════════════════════════
2323
* ChatInput — memoized input component with typewriter,
@@ -103,7 +103,8 @@ const ChatInput = memo(function ChatInput() {
103103

104104
const handleAttachClick = useCallback(() => fileInputRef.current?.click(), [])
105105

106-
const hasInput = inputValue.length > 0 || attachments.length > 0
106+
const hasContentTypes = (inputFeatures.requestedContentTypes?.length ?? 0) > 0
107+
const hasInput = inputValue.length > 0 || attachments.length > 0 || hasContentTypes
107108
const lit = hasInput && sendHover
108109

109110
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -144,10 +145,12 @@ const ChatInput = memo(function ChatInput() {
144145
if (localSendInFlightRef.current || isSending) return
145146

146147
const content = inputValue.trim()
147-
if (!content && attachments.length === 0) return
148+
if (!content && attachments.length === 0 && !hasContentTypes) return
148149
if (!canSend) return
149150

150-
const messageText = content || "See attached files."
151+
// Auto-generate prompt when user selected content types but typed nothing
152+
const messageText = content
153+
|| (hasContentTypes ? buildContentTypeAutoPrompt(inputFeatures.requestedContentTypes!) : "See attached files.")
151154
const msgAttachments = attachments.length > 0 ? attachedToMessageFormat(attachments) : undefined
152155
const previousInputValue = inputValue
153156
const previousAttachments = attachments

frontend/src/components/blocks/SessionView.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
} from "@/lib/models"
3939
import { EditIcon, TrashIcon } from "@/components/app/Icons"
4040
import ContentTypePicker, { ContentTypeChips } from "@/components/ui/ContentTypePicker"
41-
import { readStoredContentTypes, writeStoredContentTypes } from "@/lib/contentTypes"
41+
import { readStoredContentTypes, writeStoredContentTypes, buildContentTypeAutoPrompt } from "@/lib/contentTypes"
4242
import type { BlockActionRequest } from "@/types/block-actions"
4343

4444
/* ═══════════════════════════════════════════════════════════
@@ -2200,6 +2200,7 @@ export function SessionView({ initialSession, backendSessionId, readOnly = false
22002200
onDismissHighlightQuote={() => setHighlightQuote(null)}
22012201
contentTypes={inputContentTypes}
22022202
onContentTypesChange={setInputContentTypes}
2203+
topicHint={activeBlock?.title || activeBlock?.originQuestion}
22032204
/>
22042205
</div>
22052206
</div>
@@ -2852,6 +2853,8 @@ interface BlockChatInputBarProps {
28522853
/** Content type picker */
28532854
contentTypes?: string[]
28542855
onContentTypesChange?: (ids: string[]) => void
2856+
/** Topic hint for auto-prompt when user sends with content types but no text */
2857+
topicHint?: string
28552858
}
28562859

28572860
export function BlockChatInputBar({
@@ -2871,6 +2874,7 @@ export function BlockChatInputBar({
28712874
onDismissHighlightQuote,
28722875
contentTypes: contentTypesProp,
28732876
onContentTypesChange,
2877+
topicHint,
28742878
}: BlockChatInputBarProps) {
28752879
const COLORS = useThemeColors()
28762880
const isDark = useIsDark()
@@ -2919,7 +2923,8 @@ export function BlockChatInputBar({
29192923
)
29202924
const mic = useSpeechRecognition(onTranscript)
29212925

2922-
const hasInput = value.length > 0 || attachments.length > 0 || !!highlightQuote
2926+
const hasContentTypes = contentTypes.length > 0
2927+
const hasInput = value.length > 0 || attachments.length > 0 || !!highlightQuote || hasContentTypes
29232928
const lit = hasInput && sendHover
29242929

29252930
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -2946,16 +2951,19 @@ export function BlockChatInputBar({
29462951

29472952
const doSend = useCallback(async () => {
29482953
const text = value.trim()
2949-
if (!text && attachments.length === 0 && !highlightQuote) return
2954+
if (!text && attachments.length === 0 && !highlightQuote && !hasContentTypes) return
29502955
if (!canSend) return
2951-
const didSend = await onSend(text || " ", attachments, webSearch, {
2956+
// Auto-generate prompt when user selected content types but typed nothing
2957+
const sendText = text
2958+
|| (hasContentTypes ? buildContentTypeAutoPrompt(contentTypes, topicHint) : " ")
2959+
const didSend = await onSend(sendText, attachments, webSearch, {
29522960
preparedUploadIds: preparedUploadIds.length ? preparedUploadIds : undefined,
29532961
preparedBundleId,
29542962
})
29552963
if (!didSend) return
29562964
onChange("")
29572965
clearAttachments()
2958-
}, [value, attachments, onSend, onChange, webSearch, highlightQuote, canSend, preparedUploadIds, preparedBundleId, clearAttachments])
2966+
}, [value, attachments, onSend, onChange, webSearch, highlightQuote, hasContentTypes, contentTypes, topicHint, canSend, preparedUploadIds, preparedBundleId, clearAttachments])
29592967

29602968
const bg = isDark ? "#0A0A0A" : "#F8F8F8"
29612969
const border = isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.18)"

frontend/src/lib/contentTypes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ export const CONTENT_TYPE_ICONS: Record<string, string> = {
6868
simulation: "M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z",
6969
}
7070

71+
/** Build an auto-prompt when user sends with content types selected but no custom text.
72+
* Uses the active block's topic as context so the LLM generates relevant content. */
73+
export function buildContentTypeAutoPrompt(ids: string[], topicHint?: string): string {
74+
const labels = ids
75+
.map((id) => CONTENT_TYPE_MAP[id]?.label)
76+
.filter(Boolean)
77+
if (labels.length === 0) return ""
78+
const joined = labels.length === 1
79+
? labels[0].toLowerCase()
80+
: labels.slice(0, -1).map((l) => l.toLowerCase()).join(", ") + " and " + labels[labels.length - 1].toLowerCase()
81+
if (topicHint) {
82+
return `Generate ${joined} about "${topicHint}".`
83+
}
84+
return `Generate ${joined} based on what we've covered so far.`
85+
}
86+
7187
const STORAGE_KEY = "fp-requested-content-types"
7288

7389
export function readStoredContentTypes(): string[] {

0 commit comments

Comments
 (0)