Skip to content

Commit c1468db

Browse files
committed
fix(study): surface image paste failures
1 parent f19d511 commit c1468db

8 files changed

Lines changed: 382 additions & 119 deletions

File tree

plugin/plugins/study_companion/entry_common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ def _entry_exception_error(
220220
return Err(SdkError(str(exc) if message is None else message))
221221

222222

223+
def _validate_optional_vision_image_payload(
224+
owner: Any,
225+
image_base64: str,
226+
*,
227+
operation: str,
228+
):
229+
image_payload = str(image_base64 or "").strip()
230+
if not image_payload:
231+
return ""
232+
if not bool(getattr(owner._cfg, "llm_vision_enabled", False)):
233+
return Err(SdkError("llm_vision_enabled is not enabled"))
234+
try:
235+
return _normalize_submitted_image_payload(image_payload)
236+
except ValueError as exc:
237+
return _entry_exception_error(owner, exc, operation=operation)
238+
239+
223240
__all__ = [
224241
"Any",
225242
"Mapping",
@@ -297,6 +314,7 @@ def _entry_exception_error(
297314
"_validated_pomodoro_focus_minutes",
298315
"_detect_mastery_threshold_crossed",
299316
"_normalize_submitted_image_payload",
317+
"_validate_optional_vision_image_payload",
300318
"_plugin_lock",
301319
"_entry_exception_error",
302320
"_event_ratio",

plugin/plugins/study_companion/entry_tutor_answer_entries.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Ok,
77
SdkError,
88
_entry_exception_error,
9-
_normalize_submitted_image_payload,
9+
_validate_optional_vision_image_payload,
1010
plugin_entry,
1111
tr,
1212
LLM_OPERATION_ANSWER_EVALUATE,
@@ -56,17 +56,12 @@ async def study_evaluate_answer(
5656
if not resolved_question:
5757
return Err(SdkError("study tutor requires a question to evaluate against"))
5858
vision_image_payload = str(kwargs.get("vision_image_base64") or "").strip()
59-
if vision_image_payload:
60-
if not bool(self._cfg.llm_vision_enabled):
61-
return Err(SdkError("llm_vision_enabled is not enabled"))
62-
try:
63-
vision_image_payload = _normalize_submitted_image_payload(
64-
vision_image_payload
65-
)
66-
except ValueError as exc:
67-
return _entry_exception_error(
68-
self, exc, operation="study_evaluate_answer"
69-
)
59+
validated_vision_image = _validate_optional_vision_image_payload(
60+
self, vision_image_payload, operation="study_evaluate_answer"
61+
)
62+
if isinstance(validated_vision_image, Err):
63+
return validated_vision_image
64+
vision_image_payload = validated_vision_image
7065
resolved_expected = supplied_expected
7166
if not resolved_expected and (
7267
not supplied_question or supplied_question == state_question

plugin/plugins/study_companion/entry_tutor_explain_entries.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
SdkError,
88
_entry_exception_error,
99
_normalize_submitted_image_payload,
10+
_validate_optional_vision_image_payload,
1011
_plugin_lock,
1112
plugin_entry,
1213
tr,
@@ -146,11 +147,12 @@ async def study_explain_text(
146147
"source_text": source_text,
147148
}
148149
if vision_image_payload:
149-
if not bool(self._cfg.llm_vision_enabled):
150-
return Err(SdkError("llm_vision_enabled is not enabled"))
151-
vision_image_payload = _normalize_submitted_image_payload(
152-
vision_image_payload,
150+
validated_vision_image = _validate_optional_vision_image_payload(
151+
self, vision_image_payload, operation="study_explain_text"
153152
)
153+
if isinstance(validated_vision_image, Err):
154+
return validated_vision_image
155+
vision_image_payload = validated_vision_image
154156
extra_context["vision_enabled"] = True
155157
extra_context["vision_image_base64"] = vision_image_payload
156158
tutor_context = await self._build_learning_context(

plugin/plugins/study_companion/entry_tutor_question_entries.py

Lines changed: 57 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Ok,
66
SdkError,
77
_entry_exception_error,
8-
_normalize_submitted_image_payload,
8+
_validate_optional_vision_image_payload,
99
plugin_entry,
1010
tr,
1111
LLM_OPERATION_QUESTION_GENERATE,
@@ -62,56 +62,60 @@ async def study_generate_question(
6262
code="MISSING_TEXT",
6363
)
6464
)
65-
if vision_image_payload:
66-
if not bool(self._cfg.llm_vision_enabled):
67-
return Err(SdkError("llm_vision_enabled is not enabled"))
68-
try:
69-
vision_image_payload = _normalize_submitted_image_payload(
70-
vision_image_payload
71-
)
72-
except ValueError as exc:
73-
return _entry_exception_error(
74-
self, exc, operation="study_generate_question"
75-
)
76-
async with self._lock:
77-
active_mode = self._state.active_mode
78-
tutor_context = await self._build_learning_context(
79-
LLM_OPERATION_QUESTION_GENERATE,
80-
input_text=source_text,
81-
extra={
82-
"source": "ocr_snapshot"
83-
if used_ocr_fallback
84-
else ("vision_image" if vision_image_payload and not source_text else "manual"),
85-
"source_text": source_text,
86-
"topic_hint": str(topic or "").strip(),
87-
"mode": active_mode,
88-
**(
89-
{
90-
"vision_enabled": True,
91-
"vision_image_base64": vision_image_payload,
92-
}
93-
if vision_image_payload
94-
else {}
95-
),
96-
},
97-
)
98-
reply = await self._agent.question_generate(
99-
source_text, mode=active_mode, context=tutor_context
100-
)
101-
payload = await self._finalize_tutor_call(
102-
LLM_OPERATION_QUESTION_GENERATE,
103-
reply,
104-
history_kind=LLM_OPERATION_QUESTION_GENERATE,
105-
metadata={
106-
"degraded": reply.degraded,
107-
"diagnostic": reply.diagnostic,
108-
"payload": reply.payload,
109-
"screen_classification": tutor_context.get("screen_classification")
110-
or {},
111-
},
112-
extra_context=tutor_context,
113-
)
114-
payload["screen_classification"] = (
115-
tutor_context.get("screen_classification") or {}
65+
validated_vision_image = _validate_optional_vision_image_payload(
66+
self, vision_image_payload, operation="study_generate_question"
11667
)
117-
return Ok(payload)
68+
if isinstance(validated_vision_image, Err):
69+
return validated_vision_image
70+
vision_image_payload = validated_vision_image
71+
try:
72+
async with self._lock:
73+
active_mode = self._state.active_mode
74+
tutor_context = await self._build_learning_context(
75+
LLM_OPERATION_QUESTION_GENERATE,
76+
input_text=source_text,
77+
extra={
78+
"source": "ocr_snapshot"
79+
if used_ocr_fallback
80+
else (
81+
"vision_image"
82+
if vision_image_payload and not source_text
83+
else "manual"
84+
),
85+
"source_text": source_text,
86+
"topic_hint": str(topic or "").strip(),
87+
"mode": active_mode,
88+
**(
89+
{
90+
"vision_enabled": True,
91+
"vision_image_base64": vision_image_payload,
92+
}
93+
if vision_image_payload
94+
else {}
95+
),
96+
},
97+
)
98+
reply = await self._agent.question_generate(
99+
source_text, mode=active_mode, context=tutor_context
100+
)
101+
payload = await self._finalize_tutor_call(
102+
LLM_OPERATION_QUESTION_GENERATE,
103+
reply,
104+
history_kind=LLM_OPERATION_QUESTION_GENERATE,
105+
metadata={
106+
"degraded": reply.degraded,
107+
"diagnostic": reply.diagnostic,
108+
"payload": reply.payload,
109+
"screen_classification": tutor_context.get("screen_classification")
110+
or {},
111+
},
112+
extra_context=tutor_context,
113+
)
114+
payload["screen_classification"] = (
115+
tutor_context.get("screen_classification") or {}
116+
)
117+
return Ok(payload)
118+
except Exception as exc:
119+
return _entry_exception_error(
120+
self, exc, operation="study_generate_question"
121+
)

plugin/plugins/study_companion/static/style.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ textarea {
274274
outline-offset: 2px;
275275
}
276276

277+
.study-panel__paste-error {
278+
margin: 8px 0 0;
279+
color: #b42318;
280+
font-size: 13px;
281+
line-height: 1.45;
282+
}
283+
277284
.study-panel[data-busy="true"] .study-panel__image-preview {
278285
opacity: 0.55;
279286
}

0 commit comments

Comments
 (0)