Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions static/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ const LOCALES = {
session_jump_end_label: 'Jump to end of session',
session_new_message: 'New message',
session_new_message_label: 'New message available, jump to end',
jump_to_question: 'to question',
jump_to_question_label: 'Jump to the question for this response',
jump_to_question: 'to response',
jump_to_question_label: 'Jump to the start of this response',
queued_label: 'Sends after response',
queued_count: (n) => n === 1 ? '1 queued' : `${n} queued`,
queued_cancel: 'Cancel queued message',
Expand Down Expand Up @@ -1576,8 +1576,8 @@ const LOCALES = {
session_jump_end_label: 'Vai alla fine della sessione',
session_new_message: 'Nuovo messaggio',
session_new_message_label: 'Nuovo messaggio disponibile, vai alla fine',
jump_to_question: 'alla domanda',
jump_to_question_label: 'Vai alla domanda di questa risposta',
jump_to_question: 'alla risposta',
jump_to_question_label: 'Vai all\'inizio di questa risposta',
queued_label: 'Inviato dopo la risposta',
queued_count: (n) => n === 1 ? '1 in coda' : `${n} in coda`,
queued_cancel: 'Annulla messaggio in coda',
Expand Down Expand Up @@ -2972,8 +2972,8 @@ const LOCALES = {
session_jump_end_label: 'セッションの末尾へ移動',
session_new_message: '新着メッセージ',
session_new_message_label: '新着メッセージがあります。末尾へ移動',
jump_to_question: '質問へ',
jump_to_question_label: 'この回答の質問へ移動',
jump_to_question: '回答へ',
jump_to_question_label: 'この回答の先頭へ移動',
queued_label: '応答後に送信',
queued_count: (n) => `${n} 件キュー中`,
queued_cancel: 'キューに入れたメッセージをキャンセル',
Expand Down Expand Up @@ -4345,8 +4345,8 @@ const LOCALES = {
session_jump_end_label: 'Перейти к концу сессии',
session_new_message: 'Новое сообщение',
session_new_message_label: 'Новое сообщение доступно, перейти к концу',
jump_to_question: 'к вопросу',
jump_to_question_label: 'Перейти к вопросу для этого ответа',
jump_to_question: 'к ответу',
jump_to_question_label: 'Перейти к началу этого ответа',
queued_label: 'Отправить после ответа',
queued_count: (n) => n === 1 ? '1 в очереди' : `${n} в очереди`,
queued_cancel: 'Отменить сообщение',
Expand Down Expand Up @@ -5686,8 +5686,8 @@ const LOCALES = {
session_jump_end_label: 'Saltar al final de la sesión',
session_new_message: 'Mensaje nuevo',
session_new_message_label: 'Mensaje nuevo disponible, saltar al final',
jump_to_question: 'a la pregunta',
jump_to_question_label: 'Saltar a la pregunta de esta respuesta',
jump_to_question: 'a la respuesta',
jump_to_question_label: 'Saltar al inicio de esta respuesta',
queued_label: 'Enviar después de la respuesta',
queued_count: (n) => n === 1 ? '1 en cola' : `${n} en cola`,
queued_cancel: 'Cancelar mensaje en cola',
Expand Down Expand Up @@ -7018,8 +7018,8 @@ const LOCALES = {
session_jump_end_label: 'Zum Ende der Sitzung springen',
session_new_message: 'Neue Nachricht',
session_new_message_label: 'Neue Nachricht verfügbar, zum Ende springen',
jump_to_question: 'zur Frage',
jump_to_question_label: 'Zur Frage dieser Antwort springen',
jump_to_question: 'zur Antwort',
jump_to_question_label: 'Zum Anfang dieser Antwort springen',
queued_label: 'Wird nach Antwort gesendet',
queued_count: (n) => n === 1 ? '1 in Warteschlange' : `${n} in Warteschlange`,
queued_cancel: 'Nachricht abbrechen',
Expand Down Expand Up @@ -8354,8 +8354,8 @@ const LOCALES = {
session_jump_end_label: '跳转到会话结尾',
session_new_message: '新消息',
session_new_message_label: '有新消息,跳转到结尾',
jump_to_question: '回到问题',
jump_to_question_label: '跳转到这条回答对应的问题',
jump_to_question: '回到回答',
jump_to_question_label: '跳转到这条回答的开头',
queued_label: '响应后发送',
queued_count: (n) => n === 1 ? '1 条排队' : `${n} 条排队`,
queued_cancel: '取消排队消息',
Expand Down Expand Up @@ -9709,8 +9709,8 @@ const LOCALES = {
session_jump_end_label: '跳至對話結尾',
session_new_message: '新訊息',
session_new_message_label: '有新訊息,跳至結尾',
jump_to_question: '回到問題',
jump_to_question_label: '跳至這則回答對應的問題',
jump_to_question: '回到回答',
jump_to_question_label: '跳至這則回答的開頭',
queued_label: '回應後傳送',
queued_count: (n) => n === 1 ? '1 已排隊' : `${n} 已排隊`,
queued_cancel: '取消排隊訊息',
Expand Down Expand Up @@ -11015,8 +11015,8 @@ const LOCALES = {
session_jump_end_label: 'Ir para o fim da sessão',
session_new_message: 'Nova mensagem',
session_new_message_label: 'Nova mensagem disponível, ir para o fim',
jump_to_question: 'para a pergunta',
jump_to_question_label: 'Ir para a pergunta desta resposta',
jump_to_question: 'para a resposta',
jump_to_question_label: 'Ir para o início desta resposta',
queued_label: 'Envia após a resposta',
queued_count: (n) => n === 1 ? '1 na fila' : `${n} na fila`,
queued_cancel: 'Cancelar mensagem na fila',
Expand Down Expand Up @@ -12293,8 +12293,8 @@ const LOCALES = {
session_jump_end_label: '세션 끝으로 이동',
session_new_message: '새 메시지',
session_new_message_label: '새 메시지가 있습니다, 끝으로 이동',
jump_to_question: '질문으로',
jump_to_question_label: '이 응답의 질문으로 이동',
jump_to_question: '응답으로',
jump_to_question_label: '이 응답의 시작으로 이동',
queued_label: 'Sends after response',
queued_count: (n) => n === 1 ? '1 queued' : `${n} queued`,
queued_cancel: 'Cancel queued message',
Expand Down Expand Up @@ -13695,8 +13695,8 @@ const LOCALES = {
session_jump_end_label: 'Aller à la fin de la session',
session_new_message: 'Nouveau message',
session_new_message_label: 'Nouveau message disponible, aller à la fin',
jump_to_question: 'à la question',
jump_to_question_label: 'Aller à la question de cette réponse',
jump_to_question: 'à la réponse',
jump_to_question_label: 'Aller au début de cette réponse',
queued_label: 'Envoie après réponse',
queued_cancel: 'Annuler le message en file d\'attente',
model_unavailable: '(indisponible)',
Expand Down Expand Up @@ -15024,8 +15024,8 @@ const LOCALES = {
session_jump_end_label: 'Oturumun sonuna atla',
session_new_message: 'Yeni mesaj',
session_new_message_label: 'Yeni mesaj mevcut, sona atla',
jump_to_question: 'sorgulamak',
jump_to_question_label: 'Bu yanıt için soruya geçin',
jump_to_question: 'yanıta',
jump_to_question_label: 'Bu yanıtın başına git',
queued_label: 'Yanıttan sonra gönderilir',
queued_count: (n) => n === 1 ? '1 queued' : `${n} queued`,
queued_cancel: 'Sıraya alınmış mesajı iptal et',
Expand Down Expand Up @@ -16421,8 +16421,8 @@ const LOCALES = {
session_jump_end_label: 'Przejdź do końca sesji',
session_new_message: 'Nowa wiadomość',
session_new_message_label: 'Dostępna nowa wiadomość, przejdź do końca',
jump_to_question: 'do pytania',
jump_to_question_label: 'Przejdź do pytania dotyczącego tej odpowiedzi',
jump_to_question: 'do odpowiedzi',
jump_to_question_label: 'Przejdź do początku tej odpowiedzi',
queued_label: 'Zostanie wysłane po odpowiedzi',
queued_count: (n) => n === 1 ? '1 w kolejce' : `${n} w kolejce`,
queued_cancel: 'Anuluj wiadomości w kolejce',
Expand Down
20 changes: 14 additions & 6 deletions static/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,11 +484,12 @@ function _userMessageDomId(rawIdx){
return `msg-user-${rawIdx}`;
}

function _questionJumpButtonHtml(questionRawIdx){
function _questionJumpButtonHtml(questionRawIdx, assistantRawIdx){
if(typeof questionRawIdx!=='number'||questionRawIdx<0) return '';
const label=t('jump_to_question')||'Question';
const title=t('jump_to_question_label')||'Jump to the question for this response';
return `<button class="msg-question-jump-btn" type="button" title="${esc(title)}" aria-label="${esc(title)}" onclick="jumpToTurnQuestion(${questionRawIdx})"><span aria-hidden="true">↑</span><span>${esc(label)}</span></button>`;
const label=t('jump_to_question')||'Response';
const title=t('jump_to_question_label')||'Jump to the start of this response';
const aIdx=(typeof assistantRawIdx==='number'&&assistantRawIdx>=0)?assistantRawIdx:-1;
return `<button class="msg-question-jump-btn" type="button" title="${esc(title)}" aria-label="${esc(title)}" onclick="jumpToTurnQuestion(${questionRawIdx},${aIdx})"><span aria-hidden="true">↑</span><span>${esc(label)}</span></button>`;
Comment thread
rodboev marked this conversation as resolved.
}

function _highlightQuestionRow(row){
Expand All @@ -499,10 +500,13 @@ function _highlightQuestionRow(row){
window.setTimeout(()=>row.classList.remove('msg-question-highlight'),1800);
}

async function jumpToTurnQuestion(questionRawIdx){
async function jumpToTurnQuestion(questionRawIdx, assistantRawIdx){
const container=$('messages');
if(!container||typeof questionRawIdx!=='number'||questionRawIdx<0) return;
const scrollToTarget=()=>{
const hasAssistant=typeof assistantRawIdx==='number'&&assistantRawIdx>=0;
const seg=hasAssistant?container.querySelector('[data-msg-idx="'+assistantRawIdx+'"]'):null;
if(seg){seg.scrollIntoView({block:'start',behavior:'smooth'});return true;}
const row=document.getElementById(_userMessageDomId(questionRawIdx));
if(!row) return false;
row.scrollIntoView({block:'center',behavior:'smooth'});
Expand Down Expand Up @@ -8162,6 +8166,10 @@ function renderMessages(options){
if(role==='user') lastQuestionRawIdx=entry.rawIdx;
else if(role==='assistant') questionRawIdxByAssistantRawIdx.set(entry.rawIdx,lastQuestionRawIdx);
}
const assistantRawIdxByQuestionRawIdx=new Map();
for(const [aIdx,qIdx] of questionRawIdxByAssistantRawIdx){
if(!assistantRawIdxByQuestionRawIdx.has(qIdx)) assistantRawIdxByQuestionRawIdx.set(qIdx,aIdx);
}
Comment thread
rodboev marked this conversation as resolved.
// #3709 (defect B): build a per-turn combined visible-answer text so the
// thinking echo-strip can de-dupe a thinking-only message (whose own visible
// body is empty) against the answer prose carried by a SIBLING message in the
Expand Down Expand Up @@ -8316,7 +8324,7 @@ function renderMessages(options){
// user loses the navigation affordance.
const _qJumpTarget=(!isUser&&!m._live)?questionRawIdxByAssistantRawIdx.get(rawIdx):undefined;
const questionJumpBtn = (_qJumpTarget!==undefined&&_qJumpTarget!==null)
? _questionJumpButtonHtml(_qJumpTarget)
? _questionJumpButtonHtml(_qJumpTarget, assistantRawIdxByQuestionRawIdx.get(_qJumpTarget)??rawIdx)
: '';
const footHtml = `<div class="msg-foot">${timeHtml}<span class="msg-actions">${editBtn}${ttsBtn}${forkBtn}${copyBtn}${retryBtn}</span>${questionJumpBtn}</div>`;

Expand Down
15 changes: 12 additions & 3 deletions tests/test_issue2246_question_jump.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@


def test_assistant_footer_gets_completed_turn_question_jump_button():
assert "function _questionJumpButtonHtml(questionRawIdx)" in UI_JS
assert "function jumpToTurnQuestion(questionRawIdx)" in UI_JS
assert "function _questionJumpButtonHtml(questionRawIdx, assistantRawIdx)" in UI_JS
assert "function jumpToTurnQuestion(questionRawIdx, assistantRawIdx)" in UI_JS
assert "const questionRawIdxByAssistantRawIdx=new Map()" in UI_JS
assert "questionRawIdxByAssistantRawIdx.set(entry.rawIdx,lastQuestionRawIdx)" in UI_JS
assert "row.id=_userMessageDomId(rawIdx)" in UI_JS
Expand All @@ -22,10 +22,19 @@ def test_assistant_footer_gets_completed_turn_question_jump_button():
# resolved target instead of isTurnFinalAssistant.
assert "const _qJumpTarget=(!isUser&&!m._live)?questionRawIdxByAssistantRawIdx.get(rawIdx):undefined;" in UI_JS
assert "const questionJumpBtn = (_qJumpTarget!==undefined&&_qJumpTarget!==null)" in UI_JS
assert "_questionJumpButtonHtml(_qJumpTarget)" in UI_JS
assert "_questionJumpButtonHtml(_qJumpTarget, assistantRawIdxByQuestionRawIdx.get(_qJumpTarget)??rawIdx)" in UI_JS
assert "msg-question-jump-btn" in UI_JS


def test_multi_segment_turn_jumps_to_first_assistant_segment():
# #3852: the reverse map assistantRawIdxByQuestionRawIdx resolves the FIRST
# assistant segment for a given question so multi-step turns (tool_call ->
# assistant -> tool_call -> assistant) scroll to the start of the response.
assert "const assistantRawIdxByQuestionRawIdx=new Map()" in UI_JS
assert "if(!assistantRawIdxByQuestionRawIdx.has(qIdx)) assistantRawIdxByQuestionRawIdx.set(qIdx,aIdx)" in UI_JS
assert "assistantRawIdxByQuestionRawIdx.get(_qJumpTarget)" in UI_JS


def test_question_jump_expands_windowed_history_and_highlights_question():
assert "_messageRenderWindowSize=Math.max(_currentMessageRenderWindowSize(),_messageRenderableMessageCount())" in UI_JS
assert "renderMessages({ preserveScroll:true })" in UI_JS
Expand Down
Loading