Skip to content

Commit 089da21

Browse files
feat(classroom): show stage thumbnail-save button for joined students
Surface the upstream Set Thumbnail button on the stage header only while a student is joined to a classroom, letting them pick which stage frame becomes their submission thumbnail. The captured frame is cached in redux (classroom.submissionThumbnail, transient/not persisted) and reused by the student-submit flow; submitting without setting one keeps the existing auto-capture behavior. Rewords the button/alert messages for the submission context (ja / ja-Hira / en) and documents the flow in docs/classroom/ui-ux.md. Closes #631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c4114ed commit 089da21

12 files changed

Lines changed: 183 additions & 16 deletions

File tree

.claude/rules/scratch-gui/smalruby-markers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ upstream ファイルに追加した Smalruby 固有コードのマーカー一
110110
| `src/containers/blocks.jsx` | Ruby-converted toolbox update deferral | `onWorkspaceUpdate` の fromRuby 分岐の `updateToolbox()``Events.disable()` 窓の外 (finally 後) へ移動。窓内では flyout 再構築の create イベントが破棄され、新規変数が `runtime.monitorBlocks` / `flyoutBlocks` に登録されずモニタチェックボックスが無反応になる問題の修正 (issue #719)。フラグ宣言部と実行部の 2 箇所 |
111111
| `src/containers/blocks.jsx` | extension category flyout scroll | `handleExtensionAdded` 末尾で `_pendingScrollToCategoryId` をセット。scratch-blocks v1 は追加カテゴリへ自動でフォーカスしたが v2 continuous toolbox はしないため、post-rebuild で新カテゴリへスクロールさせる (Issue #749 の v13.7.2 再整合で `setBlockStyle` 復元と共存) |
112112
| `src/containers/custom-procedures.jsx` | cat-blocks theme for custom procedures | `setBlocks``workspaceConfig.scratchTheme` に catblocks/classic を設定し、定義モーダルのブロックをメインエディタと同じテーマにする (Issue #749 の v13.7.2 再整合で upstream の `workspaceConfig.theme = theme` 採用と共存) |
113+
| `src/containers/stage-header.jsx` | classroom submission thumbnail | クラスルーム参加中の生徒にだけ upstream の「提出サムネイルを設定」ボタンを表示。`isStudentJoined` ヘルパー + import、`manuallySaveThumbnails`/`userOwnsProject` を joined 由来の props にマップ、`onUpdateProjectThumbnail` でキャプチャを redux にキャッシュ、`isStudentJoined` の named export (issue #631) |
113114

114115
## 関連ファイル
115116

.claude/rules/scratch-gui/smalruby-prettier-files.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ upstream (Scratch) ファイルは対象外。
246246
- `test/unit/containers/ruby-downloader.test.jsx`
247247
- `test/unit/containers/ruby-tab-project-changed.test.js`
248248
- `test/unit/containers/rubytee-modal-hoc-sanitize.test.js`
249+
- `test/unit/containers/stage-header.test.jsx`
249250
- `test/unit/empty-block-selection.test.js`
250251
- `test/unit/lib/analytics.test.js`
251252
- `test/unit/lib/auto-correct.test.js`

docs/classroom/ui-ux.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ owner または co-teacher が、別の先生を **email で招待**して共同
445445

446446
![提出確認画面](screenshots/0306-student-submit-confirm.png)
447447

448+
> **提出サムネイルの事前設定** (issue #631): クラスに参加中の生徒は、ステージ右上の
449+
> 「提出サムネイルを設定」ボタン(✓ アイコン)を押すと、その瞬間のステージ画面を
450+
> 提出サムネイルとして事前に決めておけます。設定するとそのフレームが提出時に使われ、
451+
> プレビュー (`classroom-submit-preview`) にも反映されます。未設定のまま提出した場合は
452+
> 従来どおり提出ボタンを押した瞬間のフレームが自動キャプチャされます。ボタンはクラス
453+
> 未参加時は表示されません(`state.scratchGui.classroom` の生徒セッション有無で判定)。
454+
448455
**パーツ:**
449456

450457
| 要素 | テキスト/内容 | data-testid | 操作 |
@@ -460,7 +467,7 @@ owner または co-teacher が、別の先生を **email で招待**して共同
460467

461468
「提出する」を押すと:
462469
1. プロジェクトファイル (.sb3) を保存(ファイル名はプロジェクト名 = 課題名)
463-
2. サムネイルを生成
470+
2. サムネイルを生成(事前設定済みの提出サムネイルがあればそれを使用、無ければ自動キャプチャ)
464471
3. ステージのスクリーンショットを撮影
465472
4. Presigned URL 経由で S3 にアップロード
466473
5. 完了後 → student-status に遷移

packages/scratch-gui/.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ test/unit/containers/*
313313
!test/unit/containers/ruby-downloader.test.jsx
314314
!test/unit/containers/ruby-tab-project-changed.test.js
315315
!test/unit/containers/rubytee-modal-hoc-sanitize.test.js
316+
!test/unit/containers/stage-header.test.jsx
316317

317318
# Level 3: test/unit/helpers/* — Smalruby-only, no filter needed
318319

packages/scratch-gui/src/containers/stage-header.jsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import {connect} from 'react-redux';
1010

1111
import StageHeaderComponent from '../components/stage-header/stage-header.jsx';
1212
import {showAlertWithTimeout, showStandardAlert} from '../reducers/alerts.js';
13+
// === Smalruby: Start of classroom submission thumbnail ===
14+
import {setSubmissionThumbnail} from '../reducers/classroom';
15+
16+
// A student is "joined" to a classroom once they hold an active student session.
17+
// In that state we surface the upstream "Set Thumbnail" button so the student can
18+
// pick which stage frame becomes their submission thumbnail (issue #631).
19+
const isStudentJoined = classroom =>
20+
!!(classroom && classroom.role === 'student' && classroom.classroomId && classroom.sessionToken);
21+
// === Smalruby: End of classroom submission thumbnail ===
1322

1423
const ALERT_ID = {
1524
settingThumbnail: 'settingThumbnail',
@@ -59,14 +68,24 @@ StageHeader.propTypes = {
5968

6069
const mapStateToProps = state => {
6170
const projectState = state.scratchGui.projectState;
71+
// === Smalruby: Start of classroom submission thumbnail ===
72+
// Reuse upstream's `manuallySaveThumbnails && userOwnsProject` gate to show the
73+
// thumbnail-save button only while a student is joined to a classroom.
74+
const joined = isStudentJoined(state.scratchGui.classroom);
75+
// === Smalruby: End of classroom submission thumbnail ===
6276

6377
return {
6478
stageSizeMode: state.scratchGui.stageSize.stageSize,
6579
showBranding: state.scratchGui.mode.showBranding,
6680
isFullScreen: state.scratchGui.mode.isFullScreen,
6781
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
6882

69-
projectId: projectState.projectId
83+
projectId: projectState.projectId,
84+
85+
// === Smalruby: Start of classroom submission thumbnail ===
86+
manuallySaveThumbnails: joined,
87+
userOwnsProject: joined
88+
// === Smalruby: End of classroom submission thumbnail ===
7089
};
7190

7291
};
@@ -79,10 +98,35 @@ const mapDispatchToProps = dispatch => ({
7998
onSetStageUnFull: () => dispatch(setFullScreen(false)),
8099
onShowSettingThumbnail: () => dispatch(showStandardAlert(ALERT_ID.settingThumbnail)),
81100
onShowThumbnailSuccess: () => showAlertWithTimeout(dispatch, ALERT_ID.thumbnailSuccess),
82-
onShowThumbnailError: () => showAlertWithTimeout(dispatch, ALERT_ID.thumbnailError)
101+
onShowThumbnailError: () => showAlertWithTimeout(dispatch, ALERT_ID.thumbnailError),
102+
// === Smalruby: Start of classroom submission thumbnail ===
103+
// Instead of uploading to a project host (no host in standalone Smalruby), cache the
104+
// captured stage frame in redux so the student-submit flow can attach it later.
105+
onUpdateProjectThumbnail: (projectId, blob, onSuccess, onError) => {
106+
try {
107+
const reader = new FileReader();
108+
reader.onloadend = () => {
109+
dispatch(setSubmissionThumbnail(reader.result));
110+
if (onSuccess) onSuccess();
111+
};
112+
reader.onerror = () => {
113+
if (onError) onError();
114+
};
115+
reader.readAsDataURL(blob);
116+
} catch (e) {
117+
// eslint-disable-next-line no-console
118+
console.warn('Failed to cache submission thumbnail.', e);
119+
if (onError) onError();
120+
}
121+
}
122+
// === Smalruby: End of classroom submission thumbnail ===
83123
});
84124

85125
export default connect(
86126
mapStateToProps,
87127
mapDispatchToProps
88128
)(StageHeader);
129+
130+
// === Smalruby: Start of classroom submission thumbnail ===
131+
export {isStudentJoined};
132+
// === Smalruby: End of classroom submission thumbnail ===

packages/scratch-gui/src/containers/use-student-submit.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,20 @@ const useStudentSubmit = ({
4545

4646
const handleStartSubmit = useCallback(() => {
4747
clearError();
48-
setThumbnailDataUrl(null);
49-
if (vm && vm.renderer) {
50-
getProjectThumbnail(vm, (dataUrl) => {
51-
setThumbnailDataUrl(dataUrl);
52-
});
48+
// If the student manually picked a submission thumbnail (stage-header button),
49+
// reuse it; otherwise auto-capture the current stage frame as before (#631).
50+
if (classroomState.submissionThumbnail) {
51+
setThumbnailDataUrl(classroomState.submissionThumbnail);
52+
} else {
53+
setThumbnailDataUrl(null);
54+
if (vm && vm.renderer) {
55+
getProjectThumbnail(vm, (dataUrl) => {
56+
setThumbnailDataUrl(dataUrl);
57+
});
58+
}
5359
}
5460
setPhase('student-submit-confirm');
55-
}, [vm, clearError, setPhase]);
61+
}, [vm, clearError, setPhase, classroomState.submissionThumbnail]);
5662

5763
const captureBlockScreenshots = useCallback(async () => {
5864
if (!vm || !scratchBlocks) return [];

packages/scratch-gui/src/locales/en.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export default {
66
'gui.sharedMessages.meshV1BackpackAutoMigrated':
77
'{count, plural, one {# legacy Mesh item in your backpack was} other {# legacy Mesh items in your backpack were}} automatically replaced with the new Mesh blocks.',
88
'gui.stageHeader.stageSizeMiddle': 'Switch to middle stage',
9+
'gui.stageHeader.saveThumbnail': 'Set submission thumbnail',
10+
'gui.stageHeader.saveThumbnailMessage': 'Use the current stage as your submission thumbnail?',
11+
'gui.alerts.settingThumbnail': 'Setting submission thumbnail…',
12+
'gui.alerts.thumbnailSuccess': 'Submission thumbnail set.',
13+
'gui.alerts.thumbnailError': 'Couldn’t set the submission thumbnail. Please try again later.',
914
'gui.sharedMessages.changedRubyVersionByKoshien':
1015
'This project contains the Smalruby Koshien extension, so the Ruby version has been changed to version 1.',
1116

packages/scratch-gui/src/locales/ja-Hira.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,14 @@ export default {
285285
'gui.smalruby3.alerts.rubyVersionChangeFailed':
286286
'ルビーのバージョンをへんこうできません:さきにルビーのコードのエラーをなおしてください。',
287287
'gui.stageHeader.stageSizeMiddle': 'ちゅうサイズのステージにきりかえる',
288-
'gui.stageHeader.saveThumbnail': 'サムネイルをほぞん',
289-
'gui.stageHeader.saveThumbnailMessage': 'サムネイルをせっていしてもよいですか?',
288+
'gui.stageHeader.saveThumbnail': 'ていしゅつサムネイルをせってい',
289+
'gui.stageHeader.saveThumbnailMessage': 'いまのじっこうがめんをていしゅつサムネイルにしますか?',
290290
'gui.stageHeader.thumbnailTooltipTitle': 'おしらせ 👋',
291291
'gui.stageHeader.thumbnailTooltipBody':
292-
'「<b>サムネイルをほぞん</b>」がここにいどうしました。ステージのスナップショットをとって、プロジェクトのサムネイルとしてせっていできます。',
292+
'「<b>ていしゅつサムネイルをせってい</b>」がここにいどうしました。ステージのスナップショットをとって、ていしゅつときのサムネイルとしてせっていできます。',
293+
'gui.alerts.settingThumbnail': 'サムネイルをせっていしています…',
294+
'gui.alerts.thumbnailSuccess': 'サムネイルをせっていしました。',
295+
'gui.alerts.thumbnailError': 'サムネイルをせっていできませんでした。あとでもういちどおためしください。',
293296

294297
'gui.koshienTestModal.title': 'スモウルビーこうしえんのAIをためす',
295298
'gui.googleDriveLoader.loadError': 'Google ドライブからプロジェクトのよみこみにしっぱいしました。',

packages/scratch-gui/src/locales/ja.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,14 @@ export default {
276276
'gui.smalruby3.alerts.rubyVersionChangeFailed':
277277
'ルビーのバージョンを変更できません:先にルビーのコードのエラーを直してください。',
278278
'gui.stageHeader.stageSizeMiddle': '中サイズのステージに切り替える',
279-
'gui.stageHeader.saveThumbnail': 'サムネイルを保存',
280-
'gui.stageHeader.saveThumbnailMessage': 'サムネイルを設定してもよいですか?',
279+
'gui.stageHeader.saveThumbnail': '提出サムネイルを設定',
280+
'gui.stageHeader.saveThumbnailMessage': 'いまの実行画面を提出サムネイルにしますか?',
281281
'gui.stageHeader.thumbnailTooltipTitle': 'お知らせ 👋',
282282
'gui.stageHeader.thumbnailTooltipBody':
283-
'「<b>サムネイルを保存</b>」がここに移動しました。ステージのスナップショットを撮って、プロジェクトのサムネイルとして設定できます。',
283+
'「<b>提出サムネイルを設定</b>」がここに移動しました。ステージのスナップショットを撮って、提出時のサムネイルとして設定できます。',
284+
'gui.alerts.settingThumbnail': 'サムネイルを設定しています…',
285+
'gui.alerts.thumbnailSuccess': 'サムネイルを設定しました。',
286+
'gui.alerts.thumbnailError': 'サムネイルを設定できませんでした。あとでもう一度お試しください。',
284287

285288
'gui.koshienTestModal.title': 'スモウルビー甲子園のAIを試す',
286289
'gui.googleDriveLoader.loadError': 'Google ドライブからプロジェクトの読み込みに失敗しました。',

packages/scratch-gui/src/reducers/classroom.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const CLOSE_TEACHER_MODAL = 'scratch-gui/classroom/CLOSE_TEACHER_MODAL';
55
const SET_SESSION = 'scratch-gui/classroom/SET_SESSION';
66
const CLEAR_SESSION = 'scratch-gui/classroom/CLEAR_SESSION';
77
const SET_SUBMISSION_STATUS = 'scratch-gui/classroom/SET_SUBMISSION_STATUS';
8+
const SET_SUBMISSION_THUMBNAIL = 'scratch-gui/classroom/SET_SUBMISSION_THUMBNAIL';
9+
const CLEAR_SUBMISSION_THUMBNAIL = 'scratch-gui/classroom/CLEAR_SUBMISSION_THUMBNAIL';
810
const REQUEST_RELOGIN = 'scratch-gui/classroom/REQUEST_RELOGIN';
911
const SET_TEACHER_SELECTION = 'scratch-gui/classroom/SET_TEACHER_SELECTION';
1012
const CLEAR_TEACHER_SELECTION = 'scratch-gui/classroom/CLEAR_TEACHER_SELECTION';
@@ -75,7 +77,11 @@ const loadSession = () => {
7577
const saveSession = (session) => {
7678
if (!hasLocalStorage()) return;
7779
try {
78-
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
80+
// submissionThumbnail is a (potentially large) transient in-memory data URI used
81+
// only for the current submission; it must never be persisted to localStorage.
82+
const persistable = { ...session };
83+
delete persistable.submissionThumbnail;
84+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(persistable));
7985
} catch {
8086
// Ignore storage errors
8187
}
@@ -118,6 +124,9 @@ const initialState = {
118124
joinedAt: storedSession?.joinedAt || null,
119125
submissionStatus: storedSession?.submissionStatus || null,
120126
lastSubmittedAt: storedSession?.lastSubmittedAt || null,
127+
// Data URI of the stage frame the student manually picked as their submission
128+
// thumbnail. Transient (not persisted); null falls back to auto-capture at submit.
129+
submissionThumbnail: null,
121130
reloginRequested: false,
122131
teacherSelection: null,
123132
};
@@ -165,8 +174,13 @@ const reducer = (state, action) => {
165174
joinedAt: null,
166175
submissionStatus: null,
167176
lastSubmittedAt: null,
177+
submissionThumbnail: null,
168178
reloginRequested: false,
169179
};
180+
case SET_SUBMISSION_THUMBNAIL:
181+
return { ...state, submissionThumbnail: action.submissionThumbnail };
182+
case CLEAR_SUBMISSION_THUMBNAIL:
183+
return { ...state, submissionThumbnail: null };
170184
case SET_SUBMISSION_STATUS: {
171185
const updates = {
172186
submissionStatus: action.submissionStatus,
@@ -233,6 +247,13 @@ const setSubmissionStatus = (submissionStatus, lastSubmittedAt) => ({
233247
lastSubmittedAt,
234248
});
235249

250+
const setSubmissionThumbnail = (submissionThumbnail) => ({
251+
type: SET_SUBMISSION_THUMBNAIL,
252+
submissionThumbnail,
253+
});
254+
255+
const clearSubmissionThumbnail = () => ({ type: CLEAR_SUBMISSION_THUMBNAIL });
256+
236257
const setTeacherSelection = ({ classroomId, joinCode, className, assignmentName }) => ({
237258
type: SET_TEACHER_SELECTION,
238259
classroomId,
@@ -254,6 +275,8 @@ export {
254275
clearClassroomSession,
255276
requestRelogin,
256277
setSubmissionStatus,
278+
setSubmissionThumbnail,
279+
clearSubmissionThumbnail,
257280
setTeacherSelection,
258281
clearTeacherSelection,
259282
};

0 commit comments

Comments
 (0)