feat: dictate active Today progress updates (Closes #1058)
This commit is contained in:
1 parent
daf34d7cdd
commit
688eadf17a
8 files changed
+218
-31
No files matched your search
@@ -281,6 +281,9 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.today-progress-evidence .issue-evidence-note textarea { min-height:72px; }
|
||||
.today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; }
|
||||
.today-progress-actions button { width:100%; }
|
||||
.today-progress-panel .voice-conversation { margin-top:10px; }
|
||||
.today-progress-panel .voice-conversation-controls button,
|
||||
.today-progress-panel .voice-conversation-review-actions button { min-height:44px; }
|
||||
@media(max-width:359px) { .today-progress-actions { grid-template-columns:1fr; } }
|
||||
.today-recap-sheet { position:fixed; inset:0; z-index:88; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
|
||||
.today-recap-sheet[hidden] { display:none; }
|
||||
|
||||
+10
-18
@@ -839,24 +839,15 @@
|
||||
getLogin:() => confirmedOwnerLogin,
|
||||
});
|
||||
const voiceTranscriptStore = createVoiceTranscriptStore();
|
||||
function mountConversationVoice(kind, draftSelector) {
|
||||
return createVoiceConversationCapture({
|
||||
Recognition: window.SpeechRecognition || window.webkitSpeechRecognition,
|
||||
transcriptStore: voiceTranscriptStore,
|
||||
getLogin: () => confirmedOwnerLogin,
|
||||
elements: {
|
||||
root:qs('#voice-' + kind), start:qs('#start-voice-' + kind),
|
||||
stop:qs('#stop-voice-' + kind), review:qs('#voice-' + kind + '-review'),
|
||||
transcript:qs('#voice-' + kind + '-transcript'), append:qs('#append-voice-' + kind),
|
||||
replace:qs('#replace-with-voice-' + kind), discard:qs('#discard-voice-' + kind),
|
||||
status:qs('#voice-' + kind + '-status'), draft:qs(draftSelector),
|
||||
},
|
||||
});
|
||||
}
|
||||
const issueVoiceReply = mountConversationVoice('issue-comment', '#issue-comment');
|
||||
const pullVoiceReply = mountConversationVoice('pull-comment', '#pull-comment');
|
||||
const updateVoiceReply = mountConversationVoice('update-reply', '#update-reply');
|
||||
const searchVoiceReply = mountConversationVoice('search-reply', '#search-preview-reply');
|
||||
const voiceOptions = {
|
||||
qs, Recognition:window.SpeechRecognition || window.webkitSpeechRecognition,
|
||||
transcriptStore:voiceTranscriptStore, getLogin:() => confirmedOwnerLogin,
|
||||
};
|
||||
const [issueVoiceReply, pullVoiceReply, updateVoiceReply, searchVoiceReply, todayProgressVoice] = [
|
||||
['issue-comment', '#issue-comment'], ['pull-comment', '#pull-comment'],
|
||||
['update-reply', '#update-reply'], ['search-reply', '#search-preview-reply'],
|
||||
['today-progress', '#today-progress-body'],
|
||||
].map(([kind, draftSelector]) => mountVoiceConversation({...voiceOptions, kind, draftSelector}));
|
||||
function conversationVoiceTarget(kind, item) {
|
||||
return kind + ':' + String(item?.repository || '').trim().toLowerCase() + '#' + Number(item?.number);
|
||||
}
|
||||
@@ -1976,6 +1967,7 @@
|
||||
}
|
||||
const todayProgressView = createTodayProgressView({
|
||||
progress:todayProgress, currentTarget:currentTodayProgressTarget, qs, photos:todayProgressPhotos,
|
||||
voice:todayProgressVoice,
|
||||
announce:message => { qs('#my-work-action-status').textContent = message; },
|
||||
onAdmitted:() => refreshMyWorkView(),
|
||||
});
|
||||
|
||||
@@ -1546,6 +1546,22 @@
|
||||
<p id="today-progress-target" class="small"></p>
|
||||
<label for="today-progress-body">Update</label>
|
||||
<textarea id="today-progress-body" rows="5" maxlength="2000" placeholder="What changed, what you learned, or what comes next"></textarea>
|
||||
<section class="voice-conversation" id="voice-today-progress" data-draft-label="progress update" aria-label="Dictate Today progress update" hidden>
|
||||
<div class="voice-conversation-controls">
|
||||
<button id="start-voice-today-progress" type="button">Dictate update</button>
|
||||
<button id="stop-voice-today-progress" type="button" hidden>Stop listening</button>
|
||||
</div>
|
||||
<div id="voice-today-progress-status" class="small" aria-live="polite"></div>
|
||||
<div class="voice-conversation-review" id="voice-today-progress-review" hidden>
|
||||
<label for="voice-today-progress-transcript">Review transcript</label>
|
||||
<textarea id="voice-today-progress-transcript" maxlength="2000"></textarea>
|
||||
<div class="voice-conversation-review-actions">
|
||||
<button id="append-voice-today-progress" type="button">Use transcript</button>
|
||||
<button id="replace-with-voice-today-progress" type="button" hidden>Replace update</button>
|
||||
<button id="discard-voice-today-progress" type="button">Discard</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="today-progress-evidence" aria-labelledby="today-progress-evidence-label">
|
||||
<strong id="today-progress-evidence-label">Photo evidence <span class="small">Optional · Up to 5</span></strong>
|
||||
<div class="issue-attachment-controls conversation-photo-actions">
|
||||
|
||||
@@ -132,7 +132,7 @@ maxLength = 2000, maxItems = 20 }) {
|
||||
return { load, save, discard:identity => save(identity, ''), post };
|
||||
}
|
||||
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, announce = () => {}, onAdmitted = () => {} }) {
|
||||
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, announce = () => {}, onAdmitted = () => {} }) {
|
||||
const sheet = qs('#today-progress-sheet');
|
||||
const body = qs('#today-progress-body');
|
||||
const status = qs('#today-progress-status');
|
||||
@@ -154,6 +154,7 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, announce
|
||||
}
|
||||
};
|
||||
const close = () => {
|
||||
voice?.cancel?.();
|
||||
if (sheet.open) sheet.close();
|
||||
openedTarget = null;
|
||||
};
|
||||
@@ -166,7 +167,11 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, announce
|
||||
body.value = progress.load(target.identity);
|
||||
status.textContent = 'Restoring saved photo evidence…';
|
||||
sheet.showModal();
|
||||
try { await photos?.open?.(target); status.textContent = ''; }
|
||||
try {
|
||||
await voice?.open?.(target.identity);
|
||||
await photos?.open?.(target);
|
||||
status.textContent = '';
|
||||
}
|
||||
catch (error) { status.textContent = error.message + ' You can retry by reopening this update.'; }
|
||||
body.focus();
|
||||
});
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
function createVoiceConversationCapture({
|
||||
Recognition, elements, transcriptStore = null, getLogin = () => '',
|
||||
createEvent = name => new Event(name, {bubbles:true}),
|
||||
maxLength = 0, draftLabel = '',
|
||||
}) {
|
||||
const supported = typeof Recognition === 'function';
|
||||
const label = String(draftLabel || elements.root.dataset?.draftLabel || 'reply').trim() || 'reply';
|
||||
let recognition = null;
|
||||
let target = '';
|
||||
let generation = 0;
|
||||
elements.root.hidden = !supported;
|
||||
if (!supported) {
|
||||
elements.status.textContent = 'Voice capture is unavailable; type the reply instead.';
|
||||
elements.status.textContent = 'Voice capture is unavailable; type the ' + label + ' instead.';
|
||||
}
|
||||
|
||||
const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
|
||||
const login = () => String(getLogin() || '').trim();
|
||||
const composerLimit = Number(elements.draft.maxLength);
|
||||
const limit = Number.isInteger(maxLength) && maxLength > 0 ? maxLength :
|
||||
Number.isInteger(composerLimit) && composerLimit > 0 ? composerLimit : 10000;
|
||||
|
||||
function showReview(value, recovered = false) {
|
||||
const transcript = normalize(value).slice(0, 10000);
|
||||
const transcript = normalize(value).slice(0, limit);
|
||||
elements.transcript.value = transcript;
|
||||
elements.review.hidden = false;
|
||||
elements.append.textContent = elements.draft.value.trim() ? 'Append to draft' : 'Use transcript';
|
||||
@@ -36,7 +41,7 @@ function createVoiceConversationCapture({
|
||||
elements.review.hidden = true;
|
||||
elements.status.textContent = '';
|
||||
if (!supported || !target || !transcriptStore || !login()) return;
|
||||
const recovered = normalize(await transcriptStore.load(login(), target)).slice(0, 10000);
|
||||
const recovered = normalize(await transcriptStore.load(login(), target)).slice(0, limit);
|
||||
if (opening === generation && recovered) showReview(recovered, true);
|
||||
}
|
||||
|
||||
@@ -45,13 +50,13 @@ function createVoiceConversationCapture({
|
||||
}
|
||||
|
||||
function commit(replace) {
|
||||
const transcript = normalize(elements.transcript.value).slice(0, 10000);
|
||||
const transcript = normalize(elements.transcript.value).slice(0, limit);
|
||||
elements.draft.value = replace ? transcript :
|
||||
[elements.draft.value.trim(), transcript].filter(Boolean).join('\n\n').slice(0, 10000);
|
||||
[elements.draft.value.trim(), transcript].filter(Boolean).join('\n\n').slice(0, limit);
|
||||
elements.draft.dispatchEvent(createEvent('input'));
|
||||
clearCheckpoint();
|
||||
elements.review.hidden = true;
|
||||
elements.status.textContent = 'Transcript added. Review the reply before sending.';
|
||||
elements.status.textContent = 'Transcript added. Review the ' + label + ' before sending.';
|
||||
}
|
||||
|
||||
elements.start.addEventListener('click', () => {
|
||||
@@ -121,4 +126,20 @@ function createVoiceConversationCapture({
|
||||
return {supported, open, cancel};
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined') module.exports = {createVoiceConversationCapture};
|
||||
function mountVoiceConversation({
|
||||
kind, draftSelector, qs, Recognition, transcriptStore, getLogin,
|
||||
createCapture = createVoiceConversationCapture,
|
||||
}) {
|
||||
return createCapture({
|
||||
Recognition, transcriptStore, getLogin,
|
||||
elements:{
|
||||
root:qs('#voice-' + kind), start:qs('#start-voice-' + kind),
|
||||
stop:qs('#stop-voice-' + kind), review:qs('#voice-' + kind + '-review'),
|
||||
transcript:qs('#voice-' + kind + '-transcript'), append:qs('#append-voice-' + kind),
|
||||
replace:qs('#replace-with-voice-' + kind), discard:qs('#discard-voice-' + kind),
|
||||
status:qs('#voice-' + kind + '-status'), draft:qs(draftSelector),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined') module.exports = {createVoiceConversationCapture, mountVoiceConversation};
|
||||
Reference in new issue
Block a user