Skip to content

Commit 5e14feb

Browse files
authored
Merge pull request #19 from mgifford/copilot/update-live-transcript-interface
Show interim speech as inline live card in speaker lane
2 parents 196d150 + 4903699 commit 5e14feb

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

app.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,18 +1918,85 @@ async function postChatMsg(cardData) {
19181918

19191919
const TranscriptCtrl = {
19201920
_liveEl: null,
1921+
_interimCardEl: null,
1922+
_interimProfile: null,
19211923

19221924
init() {
19231925
this._liveEl = document.getElementById('live-transcript');
19241926
},
19251927

19261928
showInterim(text) {
1927-
if (!this._liveEl) return;
1928-
this._liveEl.textContent = text;
1929-
this._liveEl.classList.toggle('speaking', text.length > 0);
19301929
if (text && !State.currentUtteranceStartedAt) {
19311930
State.currentUtteranceStartedAt = Date.now();
19321931
}
1932+
1933+
// Always clear the bottom strip; it serves as fallback only
1934+
if (this._liveEl) {
1935+
this._liveEl.textContent = '';
1936+
this._liveEl.classList.remove('speaking');
1937+
}
1938+
1939+
if (!text) {
1940+
this._removeInterimCard();
1941+
return;
1942+
}
1943+
1944+
// Show inline inside the active speaker's lane when one exists
1945+
const profile = profileById(State.activeSpeakerId);
1946+
if (profile && profile.cardsEl) {
1947+
this._updateInterimCard(text, profile);
1948+
} else if (this._liveEl) {
1949+
// Fallback: bottom strip when no lane has been created yet
1950+
this._liveEl.textContent = text;
1951+
this._liveEl.classList.add('speaking');
1952+
}
1953+
},
1954+
1955+
// Create or refresh the inline interim card for the given profile.
1956+
_updateInterimCard(text, profile) {
1957+
// If the speaker changed mid-utterance, discard the previous card
1958+
if (this._interimProfile && this._interimProfile !== profile) {
1959+
this._removeInterimCard();
1960+
}
1961+
1962+
if (!this._interimCardEl) {
1963+
const card = document.createElement('article');
1964+
card.className = 'card card-tone-mid card-interim-live';
1965+
card.setAttribute('role', 'status');
1966+
card.setAttribute('aria-live', 'polite');
1967+
card.setAttribute('aria-label', 'Speech in progress');
1968+
card.style.setProperty('--speaker-color', profile.color);
1969+
1970+
const textEl = document.createElement('span');
1971+
textEl.className = 'interim-live-text';
1972+
card.appendChild(textEl);
1973+
1974+
const dot = document.createElement('span');
1975+
dot.className = 'interim-live-dot';
1976+
dot.setAttribute('aria-hidden', 'true');
1977+
card.appendChild(dot);
1978+
1979+
profile.cardsEl.appendChild(card);
1980+
this._interimCardEl = card;
1981+
this._interimProfile = profile;
1982+
}
1983+
1984+
const textEl = this._interimCardEl.querySelector('.interim-live-text');
1985+
if (textEl) textEl.textContent = text;
1986+
1987+
// Only scroll if the user is already near the bottom to avoid interrupting review
1988+
const el = this._interimProfile.cardsEl;
1989+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 80) {
1990+
el.scrollTop = el.scrollHeight;
1991+
}
1992+
},
1993+
1994+
_removeInterimCard() {
1995+
if (this._interimCardEl) {
1996+
this._interimCardEl.remove();
1997+
this._interimCardEl = null;
1998+
this._interimProfile = null;
1999+
}
19332000
},
19342001

19352002
clearInterim() {

style.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,37 @@ body {
10301030
/* Slightly brighter while actively speaking */
10311031
.interim-text.speaking { color: rgba(241, 243, 245, 0.65); }
10321032

1033+
/* ─── Inline interim (live) card ────────────────────────────────────────────
1034+
Shown inside the active speaker's lane while speech is in progress.
1035+
Removed and replaced by the finalised card once the utterance ends.
1036+
────────────────────────────────────────────────────────────────────────── */
1037+
.card-interim-live {
1038+
opacity: 0.7;
1039+
font-style: italic;
1040+
border-left-style: dashed !important;
1041+
animation: none; /* skip the card-enter animation — text updates too rapidly */
1042+
}
1043+
1044+
.interim-live-dot {
1045+
display: inline-block;
1046+
width: 7px;
1047+
height: 7px;
1048+
border-radius: 50%;
1049+
background: var(--speaker-color, var(--accent-a));
1050+
margin-left: 0.6rem;
1051+
vertical-align: middle;
1052+
animation: interim-dot-pulse 1.2s ease-in-out infinite;
1053+
}
1054+
1055+
@keyframes interim-dot-pulse {
1056+
0%, 100% { opacity: 1; transform: scale(1); }
1057+
50% { opacity: 0.3; transform: scale(0.75); }
1058+
}
1059+
1060+
[data-theme="light"] .card-interim-live {
1061+
opacity: 0.75;
1062+
}
1063+
10331064
.lang-mismatch-hint {
10341065
flex-shrink: 0;
10351066
max-width: 45%;

0 commit comments

Comments
 (0)