');
$("#chat").append(aiHtml);
lastAiMessage = aiHtml;
}
}
if (messages.length > 0 && messages[messages.length - 1].role === "user") {
lastAiMessage = null;
}
// 백그라운드에서 스트리밍 중인 경우: 현재 누적 텍스트 표시 후 실시간 연결
if (activeStreams[requestedRoomId]) {
var stream = activeStreams[requestedRoomId];
var liveMessage = $('
' +
'
' +
'
' +
'' +
'
' +
'
');
$("#chat").append(liveMessage);
var liveText = liveMessage.find(".text");
// 현재 누적된 텍스트 즉시 표시
if (stream.accumulatedText) {
liveText.find(".loading-gif").remove();
renderStreamingText(liveText, stream.accumulatedText);
}
// 이후 청크 실시간 반영 updater 등록
var updater = function(accumulatedText) {
if (currentRoomId !== requestedRoomId) {
// 다른 방으로 이동했으면 updater 제거
var idx = stream.updaters.indexOf(updater);
if (idx !== -1) { stream.updaters.splice(idx, 1); }
return;
}
renderStreamingText(liveText, accumulatedText);
scrollChatToBottom();
};
stream.updaters.push(updater);
// 스트림 완료 시 최종 markdown 렌더링
stream.doneUpdaters.push(function(finalText) {
if (currentRoomId !== requestedRoomId) { return; }
liveText.html(converter.makeHtml(finalText));
liveText.removeClass("streaming-live");
toggleButtonState(true);
scrollChatToBottom();
});
toggleButtonState(false);
}
// Redis에 저장된 마지막 RAG 문서 복원 — 로딩 상태 먼저 표시
showRagPanelLoading();
loadRagDocsForRoom(requestedRoomId);
// 브라우저 렌더링 완료 후 스크롤 최하단 이동 (강제)
requestAnimationFrame(function() {
requestAnimationFrame(function() { scrollChatToBottom(true); });
});
// pdService 컨텍스트 복원
if (pdServiceId) {
selectedPdServiceId = pdServiceId;
// 복원할 버전 목록 지정 (bind_VersionData_By_PdService 완료 후 적용)
if (pdServiceVersionIds && pdServiceVersionIds.length > 0) {
pendingVersionRestore = pdServiceVersionIds.slice();
}
isLoadingRoom = true;
$("#selected_pdService").val(pdServiceId).trigger("change");
isLoadingRoom = false;
bind_VersionData_By_PdService();
} else {
pendingVersionRestore = null;
selectedPdServiceId = null;
selectedPdServiceVersionIds = [];
isLoadingRoom = true;
$("#selected_pdService").val(null).trigger("change");
isLoadingRoom = false;
$("#multiversion option").remove();
$("#multiversion").multipleSelect("refresh");
}
}).fail(function (err) {
});
}
function deleteChatRoom(roomId) {
$.ajax({
url: "/auth-user/api/aichat/rooms/" + roomId,
type: "DELETE"
}).done(function () {
// 현재 보고 있던 방이면 초기화
if (currentRoomId === roomId) {
startNewChat();
}
// 목록 새로고침
loadLeftPanelHistory();
}).fail(function (err) {
});
}
function startNewChat() {
navVersion++;
isSendingMessage = false;
toggleButtonState(true);
hideRagPanel();
currentRoomId = null;
lastUserQuery = null;
lastAiMessage = null;
$("#new_message").val("");
$("#chat").empty();
$("#chat_container").hide();
$("#history_readonly_banner").hide();
$("#chat_input_area").removeClass("input-pinned").show();
$("#main_content").removeClass("flex-space-between").addClass("flex-center");
// 제품/버전 선택 초기화
selectedPdServiceId = null;
selectedPdServiceVersionIds = [];
isLoadingRoom = true;
$("#selected_pdService").val(null).trigger("change");
isLoadingRoom = false;
$("#multiversion").multipleSelect("uncheckAll");
$("#multiversion option").remove();
$("#multiversion").multipleSelect("refresh");
deleteLastSelection();
// 검색 모드 해제
$("#greeting_message").show();
loadCardContext();
}
function generateStreamId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function hideCardContainer() {
$("#card_section_header").hide();
$("#card_container").hide();
}
function showCardContainer() {
$("#card_section_header").css('display', 'flex');
$("#card_container").css('display', 'flex');
}
function hideGreetingMessage() {
$("#greeting_message").hide();
}
function showChatContent() {
$("#chat_container").show();
}
function changeFlexCenterToSpaceBetween() {
$("#main_content")
.removeClass('flex-center')
.addClass('flex-space-between');
}
// 카드 영역 로드 (입력창 하단, 고정 추천질의 - Redis 관리)
function loadCardContext() {
// 활성 채팅 중이거나 스트리밍 중이거나 메시지 전송 중에는 카드 미표시
if (currentRoomId !== null || Object.keys(activeStreams).length > 0 || isSendingMessage) {
return;
}
$.ajax({
url: "/auth-user/api/aichat/cards/context",
type: "GET",
contentType: "application/json;charset=UTF-8",
dataType: "json"
}).done(function (data) {
if (!data || data.length === 0) {
hideCardContainer();
return;
}
var container = $("#card_container");
container.empty();
data.forEach(function(item) {
var escapedQuestion = $("
").text(item.question).html();
var html =
'
' +
'
' +
'
' + escapedQuestion + '
' +
'
' +
'
';
container.append(html);
});
container.off("click", ".card").on("click", ".card", function () {
var cardQuestion = $(this).data("card-question").trim();
$("#new_message").val(cardQuestion);
sendMessage();
});
showCardContainer();
}).fail(function () {
hideCardContainer();
});
}
////////////////////////////////////////////////////////////////////////////////////////
// RAG 참조 문서 패널
////////////////////////////////////////////////////////////////////////////////////////
// backgroundRoomId가 있으면 background 모드: UI 변경 없이 Redis 저장만 수행
function fetchRagDocs(query, backgroundRoomId) {
var capturedRoomId = backgroundRoomId !== undefined ? backgroundRoomId : currentRoomId;
var isSilent = backgroundRoomId !== undefined;
var t0 = performance.now();
if (!isSilent) {
showRagPanelLoading();
toggleButtonState(false); // 문서 로드 시작 시점부터 중지 버튼
}
var deferred = $.Deferred();
// 방별 fetch 진행 상태 등록
if (capturedRoomId) ragFetchInProgress[capturedRoomId] = true;
// .then() 결과는 Promise이므로 .abort()가 없어 직접 저장하면 중지 불가
currentRagRequest = $.ajax({
url: "/ai-api/chat/context",
type: "POST",
contentType: "application/json",
data: JSON.stringify({ queryText: query }),
dataType: "json"
});
currentRagRequest.then(function (result) {
currentRagRequest = null;
var ragDocs = result.ragResults || [];
var wikiDocs = [];
var keywords = result.keywords || [];
var keyword = keywords.length > 0 ? keywords.join(", ") : "";
var searchDocuments = result.searchDocuments || [];
for (var i = 0; i < searchDocuments.length; i++) {
var doc = searchDocuments[i];
wikiDocs.push({
text: stripHtml(doc.content || ""),
metadata: { source: "Wiki · " + (doc.title || keyword) }
});
}
// 최초 질문: wiki는 unchecked 상태로 저장/렌더링 (재답변 시에만 사용자 선택 반영)
if (capturedRoomId) {
saveRagDocsToRoom(capturedRoomId, wikiDocs, ragDocs, keyword, [], null);
}
// 방별 fetch 진행 상태 해제
if (capturedRoomId) delete ragFetchInProgress[capturedRoomId];
if (currentRoomId === capturedRoomId) {
ragPanelVersion++;
renderRagPanel(wikiDocs, ragDocs, keyword, [], null);
}
deferred.resolve();
}, function (xhr, status) {
currentRagRequest = null;
if (capturedRoomId) delete ragFetchInProgress[capturedRoomId];
// 사용자가 중지 버튼으로 abort한 경우 → sendChat도 실행 안 함
if (status === "abort") {
if (!isSilent && currentRoomId === capturedRoomId) renderRagPanel([], [], "");
deferred.reject("abort");
return;
}
if (isSilent || currentRoomId !== capturedRoomId) {
deferred.resolve();
return;
}
renderRagPanel([], [], "");
deferred.resolve();
});
return deferred.promise();
}
function loadRagDocsForRoom(roomId) {
var capturedPanelVersion = ragPanelVersion;
$.ajax({
url: "/auth-user/api/aichat/rooms/" + roomId + "/rag-docs",
type: "GET",
dataType: "json"
}).done(function (result) {
if (currentRoomId !== roomId) {
hideRagPanel();
return;
}
var wikiDocs = result && result.wikiDocs ? result.wikiDocs : [];
var ragDocs = result && result.ragDocs ? result.ragDocs : [];
if (wikiDocs.length === 0 && ragDocs.length === 0) {
if (ragFetchInProgress[roomId]) return;
if (ragPanelVersion !== capturedPanelVersion) return;
}
renderRagPanel(
wikiDocs,
ragDocs,
result && result.keyword ? result.keyword : "",
result && result.checkedWikiIndexList ? result.checkedWikiIndexList : null,
result && result.checkedRagIndexList ? result.checkedRagIndexList : null
);
$(".rag-doc-checkbox").prop("disabled", true);
});
}
function saveRagDocsToRoom(roomId, wikiDocs, ragDocs, keyword, selectedWikiIndices, selectedRagIndices) {
$.ajax({
url: "/auth-user/api/aichat/rooms/" + roomId + "/rag-docs",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
wikiDocs: wikiDocs,
ragDocs: ragDocs,
keyword: keyword || "",
checkedWikiIndexList: selectedWikiIndices !== undefined ? selectedWikiIndices : null,
checkedRagIndexList: selectedRagIndices !== undefined ? selectedRagIndices : null
})
});
}
function showRagPanelLoading() {
var loadingHtml = '
검색 중...
';
$("#rag_wiki_section .rag-section-label").html(' Wiki 문서');
$("#rag_wiki_list").empty().append(loadingHtml);
$("#rag_wiki_section").show();
$("#rag_doc_list").empty().append(loadingHtml);
$("#rag_vector_section").show();
$("#rag_side").addClass("visible");
$("#chat_container").removeClass("col-lg-12").addClass("col-lg-8");
}
function renderDocItems(list, docs, docType, selectedIndices) {
$.each(docs, function (i, doc) {
var content = doc.text || doc.content || "";
var metadata = doc.metadata || {};
var source = metadata.source || metadata.file_name || metadata.fileName || "";
var score = doc.score ? Math.round(doc.score * 100) + "%" : "";
var previewText = content.length > 150 ? content.substring(0, 150) + "..." : content;
var checkboxId = "rag-doc-" + docType + "-" + i;
var isChecked = selectedIndices ? selectedIndices.indexOf(i) !== -1 : true;
var $item = $('');
var $label = $('');
var $checkbox = $('');
var $content = $('');
if (source) {
$content.append('
' + $("
").text(source).html() + '
');
}
$content.append('
' + $("
").text(previewText).html() + '
');
if (score) {
$content.append('
유사도 ' + score + '
');
}
$label.append($checkbox).append($content);
$item.append($label);
$item.data("doc", doc);
list.append($item);
});
}
function renderRagPanel(wikiDocs, ragDocs, keyword, selectedWikiIndices, selectedRagIndices) {
// Wiki 섹션 (상단) — 항상 표시, keyword 우측 노출
var keywordHtml = keyword
? 'keyword: ' + $("").text(keyword).html() + ''
: '';
$("#rag_wiki_section .rag-section-label").html(
' Wiki 문서' + keywordHtml
);
var wikiList = $("#rag_wiki_list");
wikiList.empty();
if (wikiDocs && wikiDocs.length > 0) {
renderDocItems(wikiList, wikiDocs, "wiki", selectedWikiIndices);
} else {
wikiList.append('
Wiki 문서가 없습니다
');
}
$("#rag_wiki_section").show();
// RAG 섹션 (하단)
var ragList = $("#rag_doc_list");
ragList.empty();
if (!ragDocs || ragDocs.length === 0) {
ragList.append('
RAG 문서가 없습니다
');
} else {
renderDocItems(ragList, ragDocs, "rag", selectedRagIndices);
}
$("#rag_vector_section").show();
// 패널 표시
$("#rag_side").addClass("visible");
$("#chat_container").removeClass("col-lg-12").addClass("col-lg-8");
// 체크박스 변경 시 선택된 문서 기반으로 재답변
$("#rag_wiki_list, #rag_doc_list")
.off("change", ".rag-doc-checkbox")
.on("change", ".rag-doc-checkbox", function() {
reSendWithSelectedDocs();
});
}
function getSelectedDocs() {
var wikiDocs = [];
var ragDocs = [];
$("#rag_wiki_list .rag-doc-item").each(function() {
if ($(this).find(".rag-doc-checkbox").prop("checked")) {
wikiDocs.push($(this).data("doc"));
}
});
$("#rag_doc_list .rag-doc-item").each(function() {
if ($(this).find(".rag-doc-checkbox").prop("checked")) {
ragDocs.push($(this).data("doc"));
}
});
return { wikiDocs: wikiDocs, ragDocs: ragDocs };
}
function stripHtml(html) {
var tmp = document.createElement("div");
tmp.innerHTML = html;
return (tmp.textContent || tmp.innerText || "").trim();
}
function buildEnrichedMessage(query, wikiDocs, ragDocs) {
var sb = query;
if (wikiDocs && wikiDocs.length > 0) {
sb += "\n\n[Wiki 문서]\n";
wikiDocs.forEach(function(doc) {
var text = doc.text || doc.content || "";
if (text) sb += text.trim() + "\n";
});
}
if (ragDocs && ragDocs.length > 0) {
sb += "\n\n[RAG 문서]\n";
ragDocs.forEach(function(doc) {
var text = doc.text || doc.content || "";
if (text) sb += text.trim() + "\n";
});
}
return sb;
}
function getSelectedIndices() {
var wikiIndices = [];
var ragIndices = [];
$("#rag_wiki_list .rag-doc-item").each(function(i) {
if ($(this).find(".rag-doc-checkbox").prop("checked")) {
wikiIndices.push(i);
}
});
$("#rag_doc_list .rag-doc-item").each(function(i) {
if ($(this).find(".rag-doc-checkbox").prop("checked")) {
ragIndices.push(i);
}
});
return { wikiIndices: wikiIndices, ragIndices: ragIndices };
}
function reSendWithSelectedDocs() {
if (!lastUserQuery || !currentRoomId) return;
if (isSendingMessage) return;
isSendingMessage = true;
toggleButtonState(false);
// 마지막 AI 메시지를 제자리에서 로딩 상태로 교체
// (remove+append 방식은 위치가 밀릴 수 있어 in-place 교체로 처리)
if (lastAiMessage) {
lastAiMessage.find(".text").html('');
} else {
lastAiMessage = createAiMessageContainer();
}
var selected = getSelectedDocs();
var indices = getSelectedIndices();
var capturedRoomId = currentRoomId;
var streamId = generateStreamId();
currentStreamId = streamId;
var params = JSON.stringify({
queryText: buildEnrichedMessage(lastUserQuery, selected.wikiDocs, selected.ragDocs),
sessionId: streamId,
roomId: capturedRoomId,
pdServiceId: selectedPdServiceId ? parseInt(selectedPdServiceId) : null,
pdServiceVersionIds: selectedPdServiceVersionIds.length > 0
? selectedPdServiceVersionIds.map(function(id) { return parseInt(id); })
: null
});
handleStreamResponse("/ai-api/chat/execute-stream", params, lastAiMessage, function(accumulatedText) {
isSendingMessage = false;
toggleButtonState(true);
trimChatMessages();
// 체크박스 상태와 함께 rag-docs 덮어쓰기 저장
var allWikiDocs = [];
var allRagDocs = [];
var savedKeyword = "";
$("#rag_wiki_list .rag-doc-item").each(function() { allWikiDocs.push($(this).data("doc")); });
$("#rag_doc_list .rag-doc-item").each(function() { allRagDocs.push($(this).data("doc")); });
var $keyword = $(".rag-wiki-keyword");
if ($keyword.length) {
savedKeyword = $keyword.text().replace("keyword: ", "").trim();
}
saveRagDocsToRoom(capturedRoomId, allWikiDocs, allRagDocs, savedKeyword, indices.wikiIndices, indices.ragIndices);
// 재답변 AI 메시지 Redis 덮어쓰기 (append 아닌 replace)
if (accumulatedText) {
$.ajax({
url: "/auth-user/api/aichat/rooms/" + capturedRoomId + "/messages/last-assistant",
type: "PUT",
contentType: "application/json;charset=UTF-8",
data: JSON.stringify({ role: "assistant", content: accumulatedText })
}).done(function() {
loadLeftPanelHistory();
});
}
}, null, false, function() {
// 에러 시 isSendingMessage 복원
isSendingMessage = false;
toggleButtonState(true);
});
}
function hideRagPanel() {
$("#rag_side").removeClass("visible");
$("#chat_container").removeClass("col-lg-8").addClass("col-lg-12");
$("#rag_wiki_list").empty();
$("#rag_wiki_section").hide();
$("#rag_doc_list").empty();
}