This commit is contained in:
kim
2026-05-04 00:41:10 +09:00
parent a7cdb06e60
commit fc88bda24b
8 changed files with 452 additions and 6 deletions
+42 -2
View File
@@ -767,6 +767,8 @@
.download-icon { font-size: 12px; line-height: 1; }
:root[data-theme="dark"] .download-btn { background: linear-gradient(135deg, #6366f1, #4f46e5); box-shadow: 0 1px 3px rgba(79,70,229,0.25); }
:root[data-theme="dark"] .download-btn:hover { background: linear-gradient(135deg, #4f46e5, #4338ca); }
.download-btn-error { display: inline-flex; align-items: center; gap: 4px; background: #fee2e2; color: #991b1b !important; padding: 4px 10px; border-radius: 6px; font-weight: 600; font-size: 12px; margin: 4px 2px; cursor: default; text-decoration: none !important; }
:root[data-theme="dark"] .download-btn-error { background: #3f1515; color: #fca5a5 !important; }
.pptx-file-row { margin: 4px 0; }
.pptx-preview-btn { display: inline-flex; align-items: center; gap: 4px; background: linear-gradient(135deg, #10b981, #059669); color: #fff; padding: 4px 10px; border-radius: 6px; border: none; font-weight: 600; font-size: 12px; cursor: pointer; margin-left: 4px; transition: all 0.15s; box-shadow: 0 1px 3px rgba(16,185,129,0.25); }
.pptx-preview-btn:hover { background: linear-gradient(135deg, #059669, #047857); transform: translateY(-1px); }
@@ -4593,6 +4595,7 @@ function renderChatMessages() {
return;
}
_dlLinkCache = {}; // reset on full re-render so moved/deleted files recheck
container.innerHTML = chatHistory.map((msg, idx) => {
const pinClass = contextPinMode ? ' pin-selectable' : '';
const selectedClass = (contextPinMode && pinnedMessageIndices.has(idx)) ? ' pin-selected' : '';
@@ -4614,6 +4617,8 @@ function renderChatMessages() {
</div>`;
}).join('');
validateDownloadLinks(container);
if (isThinking && streamingSessionId === activeChatSessionId) {
const progressHtml = currentProgressLines.length
? `<div style="margin:6px 0 8px 0;font-size:11px;line-height:1.6;color:var(--muted)">
@@ -8242,6 +8247,41 @@ function closeImageZoom() {
if (modal) modal.style.display = 'none';
}
// Cache of checked /api/files/ URLs: url -> true (exists) | false (not found)
var _dlLinkCache = {};
function markDownloadLinkError(btn) {
var href = btn.getAttribute ? btn.getAttribute('href') : (btn.href || '');
var filename = decodeURIComponent((href || '').split('/').pop() || '파일');
_dlLinkCache[href] = false;
var span = document.createElement('span');
span.className = 'download-btn-error';
span.title = '파일을 찾을 수 없음: ' + filename;
span.textContent = '⚠ ' + filename + ' (파일 없음)';
if (btn.parentNode) btn.parentNode.replaceChild(span, btn);
}
function validateDownloadLinks(container) {
var buttons = (container || document).querySelectorAll('a.download-btn[href]');
buttons.forEach(function(btn) {
var url = btn.getAttribute('href');
if (!url || url.indexOf('/api/files/') === -1) return;
if (_dlLinkCache[url] === false) { markDownloadLinkError(btn); return; }
if (_dlLinkCache[url] === true) return;
_dlLinkCache[url] = 'pending';
fetch(url, { method: 'HEAD', credentials: 'same-origin' })
.then(function(r) {
_dlLinkCache[url] = r.ok;
if (!r.ok) {
// btn may still be in DOM; re-query in case renderMessages rebuilt it
var found = (container || document).querySelector('a.download-btn[href="' + url.replace(/"/g, '\\"') + '"]');
if (found) markDownloadLinkError(found);
}
})
.catch(function() { _dlLinkCache[url] = false; });
});
}
function downloadFile(e, el) {
e.preventDefault();
e.stopPropagation();
@@ -8264,7 +8304,7 @@ function downloadFile(e, el) {
credentials: 'same-origin'
})
.then(function(r) {
if (!r.ok) throw new Error('Download failed: ' + r.status);
if (!r.ok) throw new Error(r.status);
return r.blob();
})
.then(function(blob) {
@@ -8278,7 +8318,7 @@ function downloadFile(e, el) {
URL.revokeObjectURL(blobUrl);
})
.catch(function(err) {
alert('Download failed: ' + err.message);
markDownloadLinkError(el);
});
}