Trong quá trình học tập và nghiên cứu dự án của một sinh viên năm thứ ba, các ứng dụng trò chuyện tích hợp AI được sử dụng với tần suất lớn. Các đoạn hội thoại thường kéo dài; nội dung hữu ích đôi khi nằm ở khoảng tin nhắn thứ 78, khiến việc tìm kiếm lại thông tin đòi hỏi thao tác cuộn trang liên tục.
Từ thực tế đó, tiện ích mở rộng DogEar trên trình duyệt Chrome đã được xây dựng, bổ sung biểu tượng ☆ bên dưới mỗi câu trả lời trên ChatGPT, Claude, Gemini, Perplexity và Grok. Người dùng chỉ cần đánh dấu câu trả lời, sau đó chọn lại từ danh sách để giao diện tự động cuộn thẳng về vị trí tương ứng.
Thao tác điều hướng nói trên thoạt nhìn chỉ cần một dòng mã lệnh:
element.scrollIntoView();
Tuy nhiên, khâu xử lý này đã chiếm phần lớn thời gian thực hiện dự án. Nội dung dưới đây giải thích nguyên nhân của trở ngại kỹ thuật trên và phương thức giúp thao tác chuyển trang đạt độ chính xác, ổn định.
Quá trình học tập và nghiên cứu dự án của sinh viên năm thứ ba thường diễn ra chủ yếu trên các hệ thống tương tác trí tuệ nhân tạo (AI). Các phiên hội thoại này thường có độ dài lớn. Những câu trả lời hữu ích thường nằm ở vị trí khoảng tin nhắn thứ 78, và việc tìm lại đòi hỏi thao tác cuộn trang liên tục.
Do đó, tiện ích mở rộng DogEar dành cho trình duyệt Chrome đã được xây dựng, cho phép bổ sung biểu tượng ☆ vào bên dưới mỗi câu trả lời trên ChatGPT, Claude, Gemini, Perplexity và Grok. Người dùng gắn sao cho câu trả lời, sau đó có thể chọn mục tương ứng trong danh sách để giao diện hội thoại tự động cuộn về đúng vị trí đã lưu.
Thao tác cuối cùng này về mặt lý thuyết chỉ cần một dòng mã:
element.scrollIntoView();
Tuy nhiên, phần việc này đã chiếm phần lớn thời lượng của dự án. Nội dung dưới đây làm rõ các nguyên nhân kỹ thuật và giải pháp giúp tính năng điều hướng đạt được sự ổn định.
Ba thách thức kỹ thuật cốt lõi
1. Tin nhắn thường không hiển thị sẵn trên trang.
Nền tảng Claude áp dụng kỹ thuật ảo hóa (virtualization) cho giao diện hội thoại: hệ thống chỉ duy trì các tin nhắn ở gần khung nhìn màn hình trong cấu trúc DOM và liên tục hoán đổi dữ liệu khi cuộn trang. Do đó, nếu tin nhắn thứ 78 đã được lưu nhưng phiên làm việc hiện đang ở tin nhắn thứ 400, lệnh document.querySelector sẽ không thể phát hiện, dù dữ liệu vẫn tồn tại. Trạng thái "không tìm thấy" không đồng nghĩa với việc tin nhắn "đã biến mất".
2. Các tin nhắn cũ được tải chậm (lazy loading), và mỗi lần tải sẽ làm dịch chuyển toàn bộ giao diện.
ChatGPT, Gemini và Claude không tải toàn bộ đoạn hội thoại dài cùng một lúc. Các tin nhắn cũ hơn được nạp theo từng đợt khi cuộn lên trên. Mỗi đợt dữ liệu được chèn vào phía trên cùng, khiến mọi vị trí hiển thị trên trang bị đẩy xuống dưới.
3. Các nền tảng liên tục thay đổi cấu trúc.
Năm nền tảng sở hữu năm cấu trúc mã đánh dấu riêng biệt, định kỳ thay đổi thiết kế sau mỗi vài tháng và đều không cung cấp API công khai cho tác vụ này.
Bước 1: Nhận diện lại tin nhắn
Khi một câu trả lời được gắn sao, DogEar sẽ lưu trữ một số phương thức để nhận diện lại sau đó:
Mã định danh (ID) tin nhắn do chính nền tảng cung cấp, nếu có (ví dụ: ChatGPT gắn nhãn mỗi tin nhắn bằng một ID trong thuộc tính dữ liệu - data attribute).
Mã băm (hash) của văn bản sau khi đã thu gọn khoảng trắng, nhằm hạn chế lỗi khi có sự thay đổi nhỏ về giao diện hiển thị.
Các từ đầu tiên của văn bản, được sử dụng làm phương án dự phòng cuối cùng.
Thuật toán băm FNV-1a dung lượng nhẹ được sử dụng, đáp ứng đầy đủ yêu cầu phân biệt các câu trả lời trong cùng một phiên hội thoại:
function hashText(text) {
const s = normalize(text); // collapse whitespace, trim
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return (h >>> 0).toString(16).padStart(8, '0');
}
Quy trình tìm kiếm tin nhắn sau đó được thực hiện theo chuỗi điều kiện ngắn. Nếu một mục được lưu kèm ID tin nhắn, hệ thống sẽ chỉ căn cứ vào ID đó. Phương pháp so khớp văn bản có thể dẫn đến việc nhận diện nhầm các câu trả lời tương tự, và việc điều hướng sai vị trí sẽ gây tác động tiêu cực hơn việc không thực hiện được điều hướng:
function findMessage(item) {
const answers = getMessages().filter(m => m.role === 'ai');
const found = item.msgId
? answers.find(m => m.msgId === item.msgId)
: answers.find(m => fingerprint(m.el).hash === item.hash)
|| answers.find(m => fingerprint(m.el).prefix === item.prefix);
return found ? found.el : null;
}
Cơ chế này vận hành tốt khi tin nhắn đang hiển thị trên trang. Khó khăn phát sinh khi tin nhắn không còn tồn tại trong cấu trúc hiển thị hiện thời.
Bước 2: Ghi nhớ vị trí tin nhắn theo phương pháp chính xác
Phiên bản đầu tiên lưu trữ vị trí câu trả lời dưới dạng tỷ lệ phần trăm theo chiều dọc của phiên hội thoại: "câu trả lời này nằm ở vị trí 40% tính từ trên xuống". Trên các trang web áp dụng cơ chế tải chậm, phương pháp này không đạt hiệu quả. Tại thời điểm lưu, có thể hệ thống mới chỉ tải 100 tin nhắn gần nhất. Khi thực hiện điều hướng sau đó, trang web sẽ nạp thêm các đợt tin nhắn cũ ở phía trên, làm tăng tổng chiều cao khung hội thoại, dẫn đến việc mốc 40% bị lệch sang một vị trí hoàn toàn khác.
Giải pháp được đưa ra là đo lường vị trí từ chiều ngược lại. Do các tin nhắn cũ chỉ được chèn thêm ở phần đầu trang, khoảng cách từ một câu trả lời đến đáy khung hội thoại sẽ luôn được giữ nguyên:
// Khoảng cách từ câu trả lời đến cuối cuộc trò chuyện, tính bằng px.
// Khác với tỷ lệ phần trăm, giá trị này vẫn giữ nguyên độ chính xác khi các tin nhắn cũ hơn được tải ở phía trên.
function fromEndOf(el) {
const box = scrollBoxOf(el); // khung chứa cuộn của cuộc trò chuyện
return Math.round(box.scrollHeight - offsetIn(el, box));
}
Để chuyển vị trí, DogEar tính toán mục tiêu dựa trên chiều cao hiện tại của cuộc trò chuyện:
const target = box.scrollHeight - item.fromEnd - box.clientHeight / 3;
Nếu target mang giá trị âm, chiều dài cuộc trò chuyện chưa đủ, đồng nghĩa các tin nhắn cũ hơn chưa được tải. Do đó, hệ thống duy trì vị trí ở đầu trang và để trang web tải liên tiếp từng đợt dữ liệu cho đến khi cuộc trò chuyện đạt đủ chiều cao, sau đó cuộn thẳng đến vị trí cần tìm.
Bước 3: Duy trì quá trình tải dữ liệu trên Claude
Vấn đề này đòi hỏi nhiều thời gian để phát hiện. Việc duy trì vị trí ở đầu cuộc trò chuyện trên Claude và chờ đợi sẽ không kích hoạt quá trình tải nội dung. Claude chỉ tải đợt tin nhắn tiếp theo khi khung trò chuyện đang cuộn.
Hiện tượng trên được xác định bằng cách thiết lập tiện ích mở rộng ghi nhật ký của từng lượt chuyển vị trí vào một tệp trong thư mục Downloads (thuận tiện hơn nhiều so với việc sao chép từ DevTools), sau đó so sánh nhật ký giữa các lượt chuyển thành công và các lượt bị treo.
Giải pháp xử lý là tạo một tác động cuộn nhỏ: nếu không có nội dung mới được tải sau 0,5 giây, hệ thống sẽ cuộn xuống 150 px và quay trở lại đầu trang ở khung hình tiếp theo. Đối với cơ chế của Claude, thao tác này tương đương hoạt động cuộn trang:
if (box.scrollTop <= 0 && now - grewAt > 500 && now - nudgedAt > 500) {
box.scrollTop = 150; // cuộn nhẹ xuống dưới…
nudgedAt = now;
} else if (box.scrollTop > 0) {
box.scrollTop = 0; // …và quay lại đầu trang ở khung hình tiếp theo
}
Hệ thống đồng thời theo dõi xem chiều dài cuộc trò chuyện có tiếp tục tăng hay không. Chừng nào các đợt tin nhắn mới vẫn tiếp tục được nạp, hệ thống sẽ duy trì trạng thái chờ (tối đa 45 s đối với các cuộc trò chuyện rất dài). Nếu không có dữ liệu nào được tải trong 2,5 s, hệ thống sẽ dừng chờ và bắt đầu tìm kiếm.
Bước 4: Tắt tính năng neo cuộn trang (scroll anchoring)
Ngay cả khi đã đến đúng khu vực mục tiêu, vị trí cuộn đôi khi vẫn bị trôi lệch. Các trình duyệt hiện có tính năng neo cuộn trang (scroll anchoring): khi nội dung phía trên màn hình thay đổi kích thước, trình duyệt sẽ tự động điều chỉnh vị trí cuộn nhằm giữ nguyên nội dung đang hiển thị. Cơ chế này tối ưu cho việc đọc, nhưng trong trường hợp này, khi trang web tải thêm các tin nhắn có độ dài lớn hơn ở phía trên, tính năng neo liên tục đẩy trang xuống dưới và vô hiệu hóa thao tác cuộn.
Giải pháp xử lý là áp dụng một dòng lệnh trong quá trình tìm kiếm, sau đó khôi phục lại trạng thái ban đầu:
box.style.overflowAnchor = 'none';
Bước 5: Tìm kiếm và dừng lại khi không có kết quả
Các trang web thường ước tính chiều cao của các tin nhắn nằm ngoài màn hình, trong khi các tin nhắn mới xuất hiện sau khi lưu sẽ đẩy câu trả lời lên cao hơn. Vì vậy, việc điều hướng đến vị trí fromEnd chỉ đưa khung nhìn lại gần mục tiêu chứ không bảo đảm độ chính xác tuyệt đối. Nếu câu trả lời không xuất hiện ngay tại vị trí đó:
kiểm tra phạm vi một màn hình ở phía dưới,
sau đó rà soát từng bước lên phía trên, kiểm tra trên từng khung hình,
tiếp tục rà soát xuống phía dưới.
Sau khoảng 15 giây (hoặc lâu hơn nếu cuộc trò chuyện dài vẫn đang trong quá trình tải), hệ thống sẽ dừng lại và hiển thị thông báo ngắn "Couldn't find this message" (Không tìm thấy tin nhắn này) thay vì tiếp tục cuộn vô hạn. Mỗi thao tác chuyển vị trí mới cũng sẽ hủy bỏ thao tác trước đó.
Nguồn tin: Dev.to AI — Tác giả: Rishit Sharma. Bản dịch tiếng Việt do AI thực hiện, có thể có sai sót.