karcytics-sdk 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {karcytics_sdk-2.2.0/src/karcytics_sdk.egg-info → karcytics_sdk-2.3.0}/PKG-INFO +1 -1
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/pyproject.toml +1 -1
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/academy.py +67 -1
- karcytics_sdk-2.3.0/src/karcytics_sdk/plugin/question_panel.py +214 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/tutorial_models.py +87 -7
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/tutorial_overlay.py +57 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0/src/karcytics_sdk.egg-info}/PKG-INFO +1 -1
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk.egg-info/SOURCES.txt +1 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/LICENSE +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/README.md +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/setup.cfg +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/commands/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/commands/diagnostics.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/commands/migrate.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/commands/scaffold.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/commands/security.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/cli/main.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/contrib/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/contrib/image_utils.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/ai.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/core_services.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/docs.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/marketplace_cache.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/module_status_widget.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/qt_bridge.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/sign_plugin.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/trust_manager.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/trust_overrides.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/trust_path.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/host/trust_storage.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_event_bus.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_logger.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/academy_driver.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/academy_window.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/analysis.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/autosave.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/base.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/components.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/context.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/course_complete_overlay.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/cyto_character.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/cyto_costumes.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/daemon.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/dialogs.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/draggable.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/effects.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/events.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/galactic_loader.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/galactic_loader.qml +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/history.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/interfaces.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/io.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/logging.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/managed_task.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/manifest.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/manifest_parser.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/menu_builder.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/preferences.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/lock.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/pipeline.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/ribbon.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/runtime_services.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/security_parser.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/signals.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/state.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/theme_fallback.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/toast.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/ui_daemon_runtime.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/ui_preferences.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/update_check.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/validation.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/wizard.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/worker_thread.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/workflow.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/py.typed +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/sdk_cli.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/testing/__init__.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/testing/contract.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk.egg-info/dependency_links.txt +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk.egg-info/entry_points.txt +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk.egg-info/requires.txt +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk.egg-info/top_level.txt +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_host_ai.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_host_security.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_image_utils.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_marketplace_cache.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_namespaces.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_plugin_architecture.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_plugin_ribbon.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_plugin_state.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_plugin_ui.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_sdk_cli.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_sign_plugin.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_testing_contract.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_theme_fallback.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_ui_daemon_runtime.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_workflow_context.py +0 -0
- {karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/tests/test_workflow_dialogs.py +0 -0
|
@@ -21,16 +21,27 @@ string directly as its topic key.
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import logging
|
|
24
|
+
import re
|
|
24
25
|
from collections.abc import Callable
|
|
25
26
|
from datetime import UTC, datetime
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Protocol, runtime_checkable
|
|
28
29
|
|
|
29
30
|
from .io import load_json, save_json
|
|
30
|
-
from .tutorial_models import
|
|
31
|
+
from .tutorial_models import (
|
|
32
|
+
QUESTION_CHECK,
|
|
33
|
+
BaseStep,
|
|
34
|
+
Course,
|
|
35
|
+
ForcedInteractionStep,
|
|
36
|
+
QuestionStep,
|
|
37
|
+
WaitForEventStep,
|
|
38
|
+
)
|
|
31
39
|
|
|
32
40
|
logger = logging.getLogger(__name__)
|
|
33
41
|
|
|
42
|
+
# `{answer:<question_id>}` in a step's text → the learner's recorded answer.
|
|
43
|
+
_ANSWER_PLACEHOLDER = re.compile(r"\{answer:([A-Za-z0-9_.-]+)\}")
|
|
44
|
+
|
|
34
45
|
# Canonical topic names. These match the Hub's `KarcyticsEvent` enum member
|
|
35
46
|
# names exactly (see `karcytics.core.event_bus`) so a Hub-side event bus
|
|
36
47
|
# adapter can resolve them with a plain `KarcyticsEvent[topic]` lookup.
|
|
@@ -90,6 +101,13 @@ class AcademyManager:
|
|
|
90
101
|
self.completed_courses: list[str] = []
|
|
91
102
|
self.badges: list[dict[str, Any]] = []
|
|
92
103
|
self.prerequisites_met: dict[str, str] = {} # course_id -> workflow_hash
|
|
104
|
+
# course_id -> question_id -> {"choices": [...], "attempts": n, "correct": bool}.
|
|
105
|
+
# Local only (progress.json); lets a later step — even in a later
|
|
106
|
+
# course — reveal a prediction the learner made.
|
|
107
|
+
self.answers: dict[str, dict[str, dict[str, Any]]] = {}
|
|
108
|
+
# Whether the live QuestionStep has been answered (correctly, for a
|
|
109
|
+
# "check" question); next_step() won't leave it otherwise.
|
|
110
|
+
self._question_answered = False
|
|
93
111
|
|
|
94
112
|
# State tracking
|
|
95
113
|
self.active_course: Course | None = None
|
|
@@ -121,6 +139,7 @@ class AcademyManager:
|
|
|
121
139
|
self.completed_courses = data.get("completed_courses", [])
|
|
122
140
|
self.badges = data.get("badges", [])
|
|
123
141
|
self.prerequisites_met = data.get("prerequisites_met", {})
|
|
142
|
+
self.answers = data.get("answers", {})
|
|
124
143
|
|
|
125
144
|
def _save_progress(self) -> None:
|
|
126
145
|
"""Saves current progress to disk."""
|
|
@@ -128,6 +147,7 @@ class AcademyManager:
|
|
|
128
147
|
"completed_courses": self.completed_courses,
|
|
129
148
|
"badges": self.badges,
|
|
130
149
|
"prerequisites_met": self.prerequisites_met,
|
|
150
|
+
"answers": self.answers,
|
|
131
151
|
}
|
|
132
152
|
try:
|
|
133
153
|
save_json(str(self.progress_file), data)
|
|
@@ -167,6 +187,7 @@ class AcademyManager:
|
|
|
167
187
|
|
|
168
188
|
if course.steps:
|
|
169
189
|
self.current_step = course.steps[0]
|
|
190
|
+
self._question_answered = False
|
|
170
191
|
self.active_subtask_progress = {}
|
|
171
192
|
# Subscribe immediately if the first step is a WaitForEventStep
|
|
172
193
|
if isinstance(self.current_step, WaitForEventStep):
|
|
@@ -202,6 +223,10 @@ class AcademyManager:
|
|
|
202
223
|
logger.warning("Cannot advance: not all sub-tasks completed.")
|
|
203
224
|
return
|
|
204
225
|
|
|
226
|
+
if isinstance(self.current_step, QuestionStep) and not self._question_answered:
|
|
227
|
+
logger.warning("Cannot advance: question not answered yet.")
|
|
228
|
+
return
|
|
229
|
+
|
|
205
230
|
if isinstance(self.current_step, WaitForEventStep) and not _internal_force and specific_step_id is None:
|
|
206
231
|
logger.warning(f"Cannot advance: waiting for event {self.current_step.event_name}")
|
|
207
232
|
return
|
|
@@ -222,6 +247,7 @@ class AcademyManager:
|
|
|
222
247
|
self.abandon_course()
|
|
223
248
|
return
|
|
224
249
|
|
|
250
|
+
self._question_answered = False
|
|
225
251
|
if next_id and next_id != "__complete__":
|
|
226
252
|
self.current_step = self.active_course.get_step(next_id) if self.active_course else None
|
|
227
253
|
if self.current_step:
|
|
@@ -237,6 +263,46 @@ class AcademyManager:
|
|
|
237
263
|
self.current_step = None
|
|
238
264
|
self._emit_step_changed()
|
|
239
265
|
|
|
266
|
+
# ── Questions ──────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
def record_answer(self, selected: list[int], attempts: int) -> bool:
|
|
269
|
+
"""Records the learner's answer to the live `QuestionStep`.
|
|
270
|
+
|
|
271
|
+
Returns whether it answers the question (any selection answers a
|
|
272
|
+
prediction). Only a correct answer unlocks `next_step()`; every
|
|
273
|
+
answer is saved, so attempts count how many tries it took.
|
|
274
|
+
"""
|
|
275
|
+
step = self.current_step
|
|
276
|
+
if not self.active_course or not isinstance(step, QuestionStep) or self.is_reviewing:
|
|
277
|
+
return False
|
|
278
|
+
correct = step.is_correct(set(selected))
|
|
279
|
+
if correct:
|
|
280
|
+
self._question_answered = True
|
|
281
|
+
if step.question_id:
|
|
282
|
+
self.answers.setdefault(self.active_course.id, {})[step.question_id] = {
|
|
283
|
+
"choices": [step.choices[i].text for i in sorted(selected)],
|
|
284
|
+
"attempts": attempts,
|
|
285
|
+
"correct": correct if step.kind == QUESTION_CHECK else None,
|
|
286
|
+
}
|
|
287
|
+
self._save_progress()
|
|
288
|
+
return correct
|
|
289
|
+
|
|
290
|
+
def recorded_answer(self, question_id: str) -> str | None:
|
|
291
|
+
"""The learner's recorded answer to `question_id`, from any course."""
|
|
292
|
+
for per_course in self.answers.values():
|
|
293
|
+
entry = per_course.get(question_id)
|
|
294
|
+
if entry and entry.get("choices"):
|
|
295
|
+
return " / ".join(entry["choices"])
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def format_step_text(self, text: str) -> str:
|
|
299
|
+
"""Replaces each `{answer:<question_id>}` with the recorded answer.
|
|
300
|
+
|
|
301
|
+
An unanswered placeholder becomes "(not answered)" rather than
|
|
302
|
+
leaking the raw placeholder into the bubble.
|
|
303
|
+
"""
|
|
304
|
+
return _ANSWER_PLACEHOLDER.sub(lambda m: self.recorded_answer(m.group(1)) or "(not answered)", text)
|
|
305
|
+
|
|
240
306
|
def complete_subtask(self, subtask_id: str) -> None:
|
|
241
307
|
"""Marks a sub-task as complete for the current step."""
|
|
242
308
|
if not self.active_course or not isinstance(self.current_step, ForcedInteractionStep):
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""The answer area of a `QuestionStep`, shown inside Cyto's bubble.
|
|
2
|
+
|
|
3
|
+
Kept out of `tutorial_overlay.py` (already the overlay's layout, masking and
|
|
4
|
+
placement engine): this is only the choices, the feedback line, and the
|
|
5
|
+
check → retry → reveal logic. The overlay supplies `check`, which records the
|
|
6
|
+
answer with the `AcademyManager` and says whether it was correct, and turns
|
|
7
|
+
the footer's Check button into Continue when `answered` fires.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from PyQt6.QtCore import Qt, pyqtSignal
|
|
15
|
+
from PyQt6.QtWidgets import (
|
|
16
|
+
QAbstractButton,
|
|
17
|
+
QButtonGroup,
|
|
18
|
+
QCheckBox,
|
|
19
|
+
QFrame,
|
|
20
|
+
QHBoxLayout,
|
|
21
|
+
QLabel,
|
|
22
|
+
QRadioButton,
|
|
23
|
+
QVBoxLayout,
|
|
24
|
+
QWidget,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .theme_fallback import theme_manager
|
|
28
|
+
from .tutorial_models import QUESTION_PREDICT, QuestionStep
|
|
29
|
+
|
|
30
|
+
_ROW_QSS = (
|
|
31
|
+
"QFrame#QuestionChoice {"
|
|
32
|
+
" background-color: {BG_MEDIUM}; border: 1px solid {BORDER}; border-radius: 6px;"
|
|
33
|
+
"}"
|
|
34
|
+
'QFrame#QuestionChoice[state="selected"] { border: 1px solid {ACCENT_PRIMARY}; }'
|
|
35
|
+
'QFrame#QuestionChoice[state="wrong"] { border: 1px solid {ACCENT_DANGER}; }'
|
|
36
|
+
'QFrame#QuestionChoice[state="reveal"] { border: 2px dashed {ACCENT_SUCCESS}; }'
|
|
37
|
+
'QFrame#QuestionChoice[state="correct"] { border: 2px solid {ACCENT_SUCCESS}; }'
|
|
38
|
+
"QFrame#QuestionChoice QLabel { color: {FG_PRIMARY}; background: transparent; border: none; }"
|
|
39
|
+
)
|
|
40
|
+
_FEEDBACK_QSS = (
|
|
41
|
+
"QLabel#QuestionFeedback { padding: 6px 2px; color: {FG_PRIMARY}; }"
|
|
42
|
+
'QLabel#QuestionFeedback[state="wrong"] { color: {ACCENT_DANGER}; }'
|
|
43
|
+
'QLabel#QuestionFeedback[state="correct"] { color: {ACCENT_SUCCESS}; }'
|
|
44
|
+
)
|
|
45
|
+
_HINT = "Choose an answer first."
|
|
46
|
+
_REVEAL_NOTE = "The outlined answer is correct — select it to continue."
|
|
47
|
+
_MULTI_MISSING = "Not quite — at least one correct answer is still unselected."
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _ChoiceRow(QFrame):
|
|
51
|
+
"""One answer: an indicator plus a word-wrapped label; the whole row clicks."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, text: str, indicator: QAbstractButton, width: int) -> None:
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.setObjectName("QuestionChoice")
|
|
56
|
+
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
57
|
+
self.indicator = indicator
|
|
58
|
+
self.label = QLabel(text)
|
|
59
|
+
self.label.setWordWrap(True)
|
|
60
|
+
self.label.setTextFormat(Qt.TextFormat.RichText)
|
|
61
|
+
row = QHBoxLayout(self)
|
|
62
|
+
row.setContentsMargins(10, 8, 10, 8)
|
|
63
|
+
row.setSpacing(8)
|
|
64
|
+
row.addWidget(indicator, 0, Qt.AlignmentFlag.AlignTop)
|
|
65
|
+
row.addWidget(self.label, 1)
|
|
66
|
+
self.setFixedWidth(width)
|
|
67
|
+
theme_manager.apply_style(self, _ROW_QSS)
|
|
68
|
+
|
|
69
|
+
def mousePressEvent(self, event) -> None: # noqa: N802
|
|
70
|
+
if self.indicator.isEnabled():
|
|
71
|
+
# A radio button's toggle() is a no-op when already checked;
|
|
72
|
+
# click() matches what clicking the indicator itself does.
|
|
73
|
+
self.indicator.click()
|
|
74
|
+
super().mousePressEvent(event)
|
|
75
|
+
|
|
76
|
+
def set_state(self, state: str) -> None:
|
|
77
|
+
self.setProperty("state", state)
|
|
78
|
+
style = self.style()
|
|
79
|
+
if style is not None: # re-evaluate [state=...] selectors
|
|
80
|
+
style.unpolish(self)
|
|
81
|
+
style.polish(self)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class QuestionPanel(QWidget):
|
|
85
|
+
"""Choices + feedback for one `QuestionStep`.
|
|
86
|
+
|
|
87
|
+
Signals:
|
|
88
|
+
answered: the question is answered (correctly, for a "check"
|
|
89
|
+
question) — the overlay swaps Check for Continue.
|
|
90
|
+
content_changed: feedback/reveal changed the panel's height.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
answered = pyqtSignal()
|
|
94
|
+
content_changed = pyqtSignal()
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
step: QuestionStep,
|
|
99
|
+
check: Callable[[list[int], int], bool],
|
|
100
|
+
width: int,
|
|
101
|
+
parent: QWidget | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__(parent)
|
|
104
|
+
self.setObjectName("QuestionPanel")
|
|
105
|
+
self._step = step
|
|
106
|
+
self._check = check
|
|
107
|
+
self.attempts = 0
|
|
108
|
+
self.is_answered = False
|
|
109
|
+
|
|
110
|
+
layout = QVBoxLayout(self)
|
|
111
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
112
|
+
layout.setSpacing(6)
|
|
113
|
+
|
|
114
|
+
self._group = QButtonGroup(self)
|
|
115
|
+
self._group.setExclusive(not step.multi_select)
|
|
116
|
+
self.rows: list[_ChoiceRow] = []
|
|
117
|
+
for i, choice in enumerate(step.choices):
|
|
118
|
+
indicator: QAbstractButton = QCheckBox() if step.multi_select else QRadioButton()
|
|
119
|
+
indicator.setObjectName(f"QuestionChoiceIndicator_{i}")
|
|
120
|
+
self._group.addButton(indicator, i)
|
|
121
|
+
row = _ChoiceRow(choice.text, indicator, width)
|
|
122
|
+
indicator.toggled.connect(self._on_selection_changed)
|
|
123
|
+
self.rows.append(row)
|
|
124
|
+
layout.addWidget(row)
|
|
125
|
+
|
|
126
|
+
if step.multi_select:
|
|
127
|
+
hint = QLabel("Select all that apply.")
|
|
128
|
+
hint.setObjectName("QuestionMultiHint")
|
|
129
|
+
theme_manager.apply_style(hint, "color: {FG_SECONDARY}; font-size: 12px;")
|
|
130
|
+
layout.insertWidget(0, hint)
|
|
131
|
+
|
|
132
|
+
self.feedback = QLabel("")
|
|
133
|
+
self.feedback.setObjectName("QuestionFeedback")
|
|
134
|
+
self.feedback.setWordWrap(True)
|
|
135
|
+
self.feedback.setTextFormat(Qt.TextFormat.RichText)
|
|
136
|
+
self.feedback.setFixedWidth(width)
|
|
137
|
+
self.feedback.hide()
|
|
138
|
+
theme_manager.apply_style(self.feedback, _FEEDBACK_QSS)
|
|
139
|
+
layout.addWidget(self.feedback)
|
|
140
|
+
|
|
141
|
+
# ── Public API ──────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
def selected(self) -> list[int]:
|
|
144
|
+
return [i for i, row in enumerate(self.rows) if row.indicator.isChecked()]
|
|
145
|
+
|
|
146
|
+
def submit(self) -> None:
|
|
147
|
+
"""The footer's Check button: grade the current selection."""
|
|
148
|
+
if self.is_answered:
|
|
149
|
+
return
|
|
150
|
+
selection = self.selected()
|
|
151
|
+
if not selection:
|
|
152
|
+
self._show_feedback(_HINT, "")
|
|
153
|
+
return
|
|
154
|
+
self.attempts += 1
|
|
155
|
+
if self._check(selection, self.attempts):
|
|
156
|
+
self._on_answered(selection)
|
|
157
|
+
else:
|
|
158
|
+
self._on_wrong(selection)
|
|
159
|
+
|
|
160
|
+
# ── Internals ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def _on_selection_changed(self) -> None:
|
|
163
|
+
if self.is_answered:
|
|
164
|
+
return
|
|
165
|
+
chosen = set(self.selected())
|
|
166
|
+
revealed = self.attempts >= self._step.reveal_after_attempts
|
|
167
|
+
for i, row in enumerate(self.rows):
|
|
168
|
+
if i in chosen:
|
|
169
|
+
row.set_state("selected")
|
|
170
|
+
elif revealed and i in self._step.correct_indices:
|
|
171
|
+
row.set_state("reveal")
|
|
172
|
+
else:
|
|
173
|
+
row.set_state("")
|
|
174
|
+
|
|
175
|
+
def _on_answered(self, selection: list[int]) -> None:
|
|
176
|
+
self.is_answered = True
|
|
177
|
+
predict = self._step.kind == QUESTION_PREDICT
|
|
178
|
+
for i, row in enumerate(self.rows):
|
|
179
|
+
row.indicator.setEnabled(False)
|
|
180
|
+
row.setCursor(Qt.CursorShape.ArrowCursor)
|
|
181
|
+
if i in selection:
|
|
182
|
+
row.set_state("selected" if predict else "correct")
|
|
183
|
+
parts = [self._step.choices[i].feedback for i in selection if self._step.choices[i].feedback]
|
|
184
|
+
if self._step.explanation:
|
|
185
|
+
parts.append(self._step.explanation)
|
|
186
|
+
prefix = "" if predict else "✓ Correct. "
|
|
187
|
+
self._show_feedback(prefix + " ".join(parts), "" if predict else "correct")
|
|
188
|
+
self.answered.emit()
|
|
189
|
+
|
|
190
|
+
def _on_wrong(self, selection: list[int]) -> None:
|
|
191
|
+
correct = self._step.correct_indices
|
|
192
|
+
wrong_picks = [i for i in selection if i not in correct]
|
|
193
|
+
for i in wrong_picks:
|
|
194
|
+
self.rows[i].set_state("wrong")
|
|
195
|
+
if wrong_picks:
|
|
196
|
+
message = self._step.choices[wrong_picks[0]].feedback
|
|
197
|
+
else: # multi-select with only correct picks, but not all of them
|
|
198
|
+
message = _MULTI_MISSING
|
|
199
|
+
if self.attempts >= self._step.reveal_after_attempts:
|
|
200
|
+
for i in correct:
|
|
201
|
+
if i not in selection:
|
|
202
|
+
self.rows[i].set_state("reveal")
|
|
203
|
+
message = f"{message}<br><br><b>{_REVEAL_NOTE}</b>"
|
|
204
|
+
self._show_feedback(f"✗ {message}", "wrong")
|
|
205
|
+
|
|
206
|
+
def _show_feedback(self, text: str, state: str) -> None:
|
|
207
|
+
self.feedback.setText(text)
|
|
208
|
+
self.feedback.setProperty("state", state)
|
|
209
|
+
style = self.feedback.style()
|
|
210
|
+
if style is not None:
|
|
211
|
+
style.unpolish(self.feedback)
|
|
212
|
+
style.polish(self.feedback)
|
|
213
|
+
self.feedback.show()
|
|
214
|
+
self.content_changed.emit()
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
"""SOLID models and interfaces for the Karcytics Academy tutorial engine.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`abc`/`dataclasses`/`typing`), so
|
|
6
|
-
content can depend on this permanent, real copy directly rather than an
|
|
7
|
-
unimportable Hub-internal path. Any plugin author writing course content
|
|
8
|
-
imports from here; the Hub's own copy is the authoritative implementation
|
|
9
|
-
these mirror.
|
|
3
|
+
Originally vendored from the Hub; this is now the only copy. Pure
|
|
4
|
+
dataclasses/interfaces with no dependency on the Hub (only
|
|
5
|
+
`abc`/`dataclasses`/`typing`), so plugin course content imports from here.
|
|
10
6
|
"""
|
|
11
7
|
|
|
12
8
|
from abc import ABC, abstractmethod
|
|
@@ -206,6 +202,86 @@ class ConsentStep(BaseStep):
|
|
|
206
202
|
on_decline_step_id: str | None = None
|
|
207
203
|
|
|
208
204
|
|
|
205
|
+
QUESTION_CHECK = "check"
|
|
206
|
+
QUESTION_PREDICT = "predict"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class AnswerChoice:
|
|
211
|
+
"""One answer to a `QuestionStep`.
|
|
212
|
+
|
|
213
|
+
`feedback` is shown when the learner picks this choice: for a wrong
|
|
214
|
+
choice it says *why* it's wrong (required for "check" questions), for a
|
|
215
|
+
correct one it can add a short confirmation.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
text: str
|
|
219
|
+
correct: bool = False
|
|
220
|
+
feedback: str = ""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass
|
|
224
|
+
class QuestionStep(BaseStep):
|
|
225
|
+
"""A question answered in Cyto's bubble; `text` is the question.
|
|
226
|
+
|
|
227
|
+
Two kinds:
|
|
228
|
+
|
|
229
|
+
- ``"check"`` (default): the learner must select the correct choice(s)
|
|
230
|
+
to continue. A wrong pick shows its feedback; after
|
|
231
|
+
`reveal_after_attempts` wrong tries the correct choice(s) are
|
|
232
|
+
outlined — they still have to select them.
|
|
233
|
+
- ``"predict"``: any answer is accepted and recorded under
|
|
234
|
+
`question_id`, so a later step can reveal it (``{answer:<id>}`` in a
|
|
235
|
+
step's text is replaced with the learner's choice, across courses).
|
|
236
|
+
|
|
237
|
+
`multi_select` makes it "select all that apply": correct only when the
|
|
238
|
+
selection is exactly the set of correct choices. `explanation` is shown
|
|
239
|
+
once answered, before Continue.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
choices: list[AnswerChoice] = field(default_factory=list)
|
|
243
|
+
multi_select: bool = False
|
|
244
|
+
kind: str = QUESTION_CHECK
|
|
245
|
+
question_id: str = ""
|
|
246
|
+
explanation: str = ""
|
|
247
|
+
reveal_after_attempts: int = 2
|
|
248
|
+
|
|
249
|
+
def __post_init__(self) -> None:
|
|
250
|
+
"""Rejects questions that can't be answered (or can't teach)."""
|
|
251
|
+
if self.kind not in (QUESTION_CHECK, QUESTION_PREDICT):
|
|
252
|
+
raise ValueError(f"{self.id}: kind must be 'check' or 'predict', got {self.kind!r}")
|
|
253
|
+
if len(self.choices) < 2: # noqa: PLR2004
|
|
254
|
+
raise ValueError(f"{self.id}: a question needs at least 2 choices")
|
|
255
|
+
if self.kind == QUESTION_PREDICT:
|
|
256
|
+
if not self.question_id:
|
|
257
|
+
raise ValueError(f"{self.id}: a prediction needs a question_id to reveal it later")
|
|
258
|
+
return
|
|
259
|
+
n_correct = sum(c.correct for c in self.choices)
|
|
260
|
+
if self.multi_select and n_correct < 1:
|
|
261
|
+
raise ValueError(f"{self.id}: select-all question needs at least one correct choice")
|
|
262
|
+
if not self.multi_select and n_correct != 1:
|
|
263
|
+
raise ValueError(f"{self.id}: single-choice question needs exactly one correct choice")
|
|
264
|
+
missing = [c.text for c in self.choices if not c.correct and not c.feedback]
|
|
265
|
+
if missing:
|
|
266
|
+
raise ValueError(f"{self.id}: wrong choices need feedback explaining why: {missing}")
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def correct_indices(self) -> frozenset[int]:
|
|
270
|
+
"""Indices of the correct choices (empty for a prediction)."""
|
|
271
|
+
return frozenset(i for i, c in enumerate(self.choices) if c.correct)
|
|
272
|
+
|
|
273
|
+
def is_correct(self, selected: set[int] | frozenset[int]) -> bool:
|
|
274
|
+
"""Whether `selected` (choice indices) answers the question.
|
|
275
|
+
|
|
276
|
+
Any non-empty selection answers a prediction.
|
|
277
|
+
"""
|
|
278
|
+
if not selected:
|
|
279
|
+
return False
|
|
280
|
+
if self.kind == QUESTION_PREDICT:
|
|
281
|
+
return True
|
|
282
|
+
return frozenset(selected) == self.correct_indices
|
|
283
|
+
|
|
284
|
+
|
|
209
285
|
@dataclass
|
|
210
286
|
class Course:
|
|
211
287
|
"""A collection of polymorphic steps representing a guided tutorial course."""
|
|
@@ -284,5 +360,9 @@ __all__ = [
|
|
|
284
360
|
"SubplotCheckStep",
|
|
285
361
|
"WaitForEventStep",
|
|
286
362
|
"ConsentStep",
|
|
363
|
+
"QUESTION_CHECK",
|
|
364
|
+
"QUESTION_PREDICT",
|
|
365
|
+
"AnswerChoice",
|
|
366
|
+
"QuestionStep",
|
|
287
367
|
"Course",
|
|
288
368
|
]
|
|
@@ -41,6 +41,7 @@ from .academy import (
|
|
|
41
41
|
from .cyto_character import CytoWidget
|
|
42
42
|
from .dialogs import ask_yes_no
|
|
43
43
|
from .draggable import DraggableMixin
|
|
44
|
+
from .question_panel import QuestionPanel
|
|
44
45
|
from .theme_fallback import Colors, theme_manager
|
|
45
46
|
from .tutorial_models import (
|
|
46
47
|
BaseStep,
|
|
@@ -49,6 +50,7 @@ from .tutorial_models import (
|
|
|
49
50
|
ForcedInteractionStep,
|
|
50
51
|
InfoStep,
|
|
51
52
|
InteractionStep,
|
|
53
|
+
QuestionStep,
|
|
52
54
|
SubplotCheckStep,
|
|
53
55
|
VerificationStep,
|
|
54
56
|
WaitForEventStep,
|
|
@@ -137,6 +139,8 @@ class TutorialOverlay(QWidget):
|
|
|
137
139
|
self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
|
|
138
140
|
|
|
139
141
|
self.target_rects: list[QRect] = []
|
|
142
|
+
# The live QuestionStep's answer panel, if any (see _render_question).
|
|
143
|
+
self.question_panel: QuestionPanel | None = None
|
|
140
144
|
# The widget a step's allow_scroll=True should forward wheel events
|
|
141
145
|
# to (see set_scroll_target()/wheelEvent()) — set by AcademyStepDriver
|
|
142
146
|
# from the ancestor QScrollArea of the step's own target widget(s),
|
|
@@ -442,6 +446,10 @@ class TutorialOverlay(QWidget):
|
|
|
442
446
|
self.cyto.apply_theme()
|
|
443
447
|
|
|
444
448
|
def _update_text_rendering(self, text: str) -> None:
|
|
449
|
+
# `{answer:<id>}` placeholders → the learner's recorded answers.
|
|
450
|
+
format_text = getattr(self._academy_manager, "format_step_text", None)
|
|
451
|
+
if callable(format_text):
|
|
452
|
+
text = format_text(text)
|
|
445
453
|
import re
|
|
446
454
|
|
|
447
455
|
# Replace newlines with <br>
|
|
@@ -666,6 +674,10 @@ class TutorialOverlay(QWidget):
|
|
|
666
674
|
self.btn_next.hide()
|
|
667
675
|
self._render_checklist(step)
|
|
668
676
|
|
|
677
|
+
elif isinstance(step, QuestionStep):
|
|
678
|
+
self.btn_next.hide()
|
|
679
|
+
self._render_question(step)
|
|
680
|
+
|
|
669
681
|
elif isinstance(step, SubplotCheckStep):
|
|
670
682
|
self.btn_next.show()
|
|
671
683
|
self.btn_next.setText("Confirm Subplot")
|
|
@@ -1223,6 +1235,50 @@ class TutorialOverlay(QWidget):
|
|
|
1223
1235
|
if remaining_count == 0 and not getattr(self.current_step, "auto_advance_when_complete", False):
|
|
1224
1236
|
self.btn_next.show()
|
|
1225
1237
|
|
|
1238
|
+
def _render_question(self, step: QuestionStep) -> None:
|
|
1239
|
+
"""Choices in the bubble; Check in the footer, then Continue once answered."""
|
|
1240
|
+
manager = self._academy_manager
|
|
1241
|
+
panel = QuestionPanel(
|
|
1242
|
+
step,
|
|
1243
|
+
check=lambda selection, attempts: manager.record_answer(selection, attempts),
|
|
1244
|
+
width=CONTENT_WIDTH,
|
|
1245
|
+
)
|
|
1246
|
+
self.question_panel = panel
|
|
1247
|
+
self.dynamic_content.addWidget(panel)
|
|
1248
|
+
|
|
1249
|
+
self._clear_buttons()
|
|
1250
|
+
self.btn_layout.addStretch()
|
|
1251
|
+
self.btn_layout.addWidget(self.btn_previous)
|
|
1252
|
+
self._show_previous_button()
|
|
1253
|
+
btn_check = QPushButton("Check answer")
|
|
1254
|
+
btn_check.setObjectName("QuestionCheckButton")
|
|
1255
|
+
theme_manager.apply_style(
|
|
1256
|
+
btn_check,
|
|
1257
|
+
"background-color: {ACCENT_PRIMARY}; color: white; border: none; border-radius: 4px; padding: 6px 12px; font-weight: bold;",
|
|
1258
|
+
)
|
|
1259
|
+
btn_check.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
1260
|
+
btn_check.clicked.connect(panel.submit)
|
|
1261
|
+
self.btn_layout.addWidget(btn_check)
|
|
1262
|
+
btn_check.show() # see _render_consent_options
|
|
1263
|
+
|
|
1264
|
+
def _on_answered() -> None:
|
|
1265
|
+
btn_check.clicked.disconnect()
|
|
1266
|
+
btn_check.setObjectName("QuestionContinueButton")
|
|
1267
|
+
btn_check.setText("Let's find out →" if step.kind == "predict" else "Continue →")
|
|
1268
|
+
btn_check.clicked.connect(lambda _checked=False: manager.next_step())
|
|
1269
|
+
|
|
1270
|
+
panel.answered.connect(_on_answered)
|
|
1271
|
+
panel.content_changed.connect(self._on_question_content_changed)
|
|
1272
|
+
|
|
1273
|
+
def _on_question_content_changed(self) -> None:
|
|
1274
|
+
"""Feedback appeared or grew: re-fit the bubble and re-punch the mask."""
|
|
1275
|
+
if not self._is_alive():
|
|
1276
|
+
return
|
|
1277
|
+
self._force_resize()
|
|
1278
|
+
if not self._user_positioned:
|
|
1279
|
+
self._reposition_cyto_and_bubble(getattr(self, "target_rects", []))
|
|
1280
|
+
self._update_mask()
|
|
1281
|
+
|
|
1226
1282
|
def _render_branching_options(self, options: dict) -> None:
|
|
1227
1283
|
"""Render branching-option buttons that advance to their selected tutorial steps.
|
|
1228
1284
|
|
|
@@ -1379,6 +1435,7 @@ class TutorialOverlay(QWidget):
|
|
|
1379
1435
|
self._update_mask()
|
|
1380
1436
|
|
|
1381
1437
|
def _clear_dynamic_content(self) -> None:
|
|
1438
|
+
self.question_panel = None
|
|
1382
1439
|
# Stop any running WaitForEventStep pulse timer
|
|
1383
1440
|
if hasattr(self, "_wait_pulse_timer") and self._wait_pulse_timer.isActive():
|
|
1384
1441
|
self._wait_pulse_timer.stop()
|
|
@@ -64,6 +64,7 @@ src/karcytics_sdk/plugin/manifest.py
|
|
|
64
64
|
src/karcytics_sdk/plugin/manifest_parser.py
|
|
65
65
|
src/karcytics_sdk/plugin/menu_builder.py
|
|
66
66
|
src/karcytics_sdk/plugin/preferences.py
|
|
67
|
+
src/karcytics_sdk/plugin/question_panel.py
|
|
67
68
|
src/karcytics_sdk/plugin/ribbon.py
|
|
68
69
|
src/karcytics_sdk/plugin/runtime_services.py
|
|
69
70
|
src/karcytics_sdk/plugin/security_parser.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/course_complete_overlay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py
RENAMED
|
File without changes
|
|
File without changes
|
{karcytics_sdk-2.2.0 → karcytics_sdk-2.3.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|