Skip to content
Open
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
88 changes: 79 additions & 9 deletions src/components/AiChatWidget.astro
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
<!-- Floating popup icon (shown in sidebar mode) -->
<svg id="ai-sb-undock" class="w-3.5 h-3.5 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="12" height="12" rx="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M20 8v12a2 2 0 01-2 2H8" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
<button id="ai-minimize-btn" class="p-1 rounded-md text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] transition-colors cursor-pointer" title="Minimize" aria-label="Minimize AI chat">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14" stroke-linecap="round" /></svg>
</button>
<button id="ai-fullscreen-btn" class="p-1 rounded-md text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] transition-colors cursor-pointer" title="Toggle fullscreen">
<svg id="ai-fs-expand" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" stroke-linecap="round" stroke-linejoin="round" /></svg>
<svg id="ai-fs-shrink" class="w-3.5 h-3.5 hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" stroke-linecap="round" stroke-linejoin="round" /></svg>
Expand Down Expand Up @@ -127,6 +130,11 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
</div>
</div>

<button id="ai-restore-chip" type="button" class="hidden fixed z-50 items-center gap-2 rounded-full border border-[var(--color-border-default)] px-3 py-2 text-xs font-medium text-[var(--color-text-primary)] shadow-lg cursor-pointer" style="background:#161616;bottom:24px;right:24px;min-height:44px;min-width:44px;" aria-label="Restore AI chat" title="Restore AI chat">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" stroke-linecap="round" stroke-linejoin="round" /></svg>
<span>Restore AI chat</span>
</button>

<script is:inline>
(function() {
function setupWidget() {
Expand All @@ -150,6 +158,8 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
var quickBtns = document.querySelectorAll('.ai-wq');

var sidebarBtn = document.getElementById('ai-sidebar-btn');
var minimizeBtn = document.getElementById('ai-minimize-btn');
var restoreChip = document.getElementById('ai-restore-chip');
var resizeTop = document.getElementById('ai-resize-top');
var resizeLeft = document.getElementById('ai-resize-left');

Expand Down Expand Up @@ -248,6 +258,7 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';

// Restore state from sessionStorage (persists across Astro page navigations)
var isOpen = sessionStorage.getItem('ai-chat-open') === 'true';
var isMinimized = sessionStorage.getItem('ai-chat-minimized') === 'true';
var conversationHistory = [];
try {
var stored = sessionStorage.getItem('ai-chat-history');
Expand All @@ -256,7 +267,19 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';

function saveState() {
sessionStorage.setItem('ai-chat-open', isOpen ? 'true' : 'false');
sessionStorage.setItem('ai-chat-minimized', isMinimized ? 'true' : 'false');
sessionStorage.setItem('ai-chat-history', JSON.stringify(conversationHistory.slice(-20)));
if (input) sessionStorage.setItem('ai-chat-draft', input.value);
}
function syncRestoreChip() {
if (!restoreChip) return;
if (isMinimized && !isOpen) {
restoreChip.classList.remove('hidden');
restoreChip.classList.add('flex');
} else {
restoreChip.classList.add('hidden');
restoreChip.classList.remove('flex');
}
}
function saveMessagesHtml() {
if (messagesEl) {
Expand Down Expand Up @@ -460,7 +483,9 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';

function open() {
isOpen = true;
isMinimized = false;
saveState();
syncRestoreChip();
applyDefault();
// Lazy-load chat dependencies (marked.js, highlight.js, Turnstile) — await before allowing messages
if (typeof window.loadChatDeps === 'function') {
Expand All @@ -477,10 +502,38 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
if (input) setTimeout(function() { input.focus(); }, 100);
}

function minimize() {
if (!isOpen) return;
var remembered = isFullscreen ? 'fullscreen' : (isSidebar ? 'sidebar' : 'popup');
if (isSidebar) exitSidebar();
if (isFullscreen) exitFullscreen();
sessionStorage.setItem('ai-chat-restore-mode', remembered);
isOpen = false;
isMinimized = true;
saveState();
saveMessagesHtml();
popup.style.opacity = '0';
popup.style.transform = 'translateY(8px) scale(0.97)';
setTimeout(function() { popup.classList.add('hidden'); popup.classList.remove('flex'); popup.style.transition = ''; }, 200);
syncRestoreChip();
if (restoreChip) restoreChip.focus();
}

function restore() {
isMinimized = false;
var mode = sessionStorage.getItem('ai-chat-restore-mode') || 'popup';
open();
if (mode === 'sidebar' && window.innerWidth >= 768) enterSidebar();
else if (mode === 'fullscreen') enterFullscreen();
saveState();
}

function close() {
if (isFullscreen) exitFullscreen();
isOpen = false;
isMinimized = false;
saveState();
syncRestoreChip();
popup.style.opacity = '0';
popup.style.transform = 'translateY(8px) scale(0.97)';
setTimeout(function() { popup.classList.add('hidden'); popup.classList.remove('flex'); popup.style.transition = ''; }, 200);
Expand Down Expand Up @@ -591,20 +644,32 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
if (isFullscreen) exitFullscreen();
isSidebar ? exitSidebar() : enterSidebar();
});
if (newChatBtn) newChatBtn.addEventListener('click', resetChat);
if (newChatBtn) newChatBtn.addEventListener('click', function() {
isMinimized = false;
sessionStorage.removeItem('ai-chat-draft');
if (input) input.value = '';
resetChat();
syncRestoreChip();
});
if (minimizeBtn) minimizeBtn.addEventListener('click', minimize);
if (restoreChip) restoreChip.addEventListener('click', restore);
// Stop button kept for backward compat but send button is primary stop
if (stopBtn) stopBtn.addEventListener('click', function() { if (currentAbortController) currentAbortController.abort(); });

window.openAiChat = function() { if (!isOpen) open(); };
window.openAiChat = function() {
if (isMinimized) restore();
else if (!isOpen) open();
};
window._openAiChatInner = function() { if (!isOpen) open(); };
window.closeAiChat = function() { if (isSidebar) exitSidebar(); if (isOpen) close(); };

document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isOpen) {
if (isFullscreen) exitFullscreen();
else if (isSidebar) { exitSidebar(); close(); }
else close();
}
if (e.key !== 'Escape' || !isOpen) return;
if (isFullscreen) { exitFullscreen(); return; }
var focusInside = popup.contains(document.activeElement);
if (!focusInside) return;
// minimize() records sidebar mode before undocking, so Restore re-docks
minimize();
});

// --- Resize (corner + top edge + left edge) ---
Expand Down Expand Up @@ -677,7 +742,9 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// Restore open state and mode
if (isOpen) {
if (isMinimized) {
syncRestoreChip();
} else if (isOpen) {
var savedMode = sessionStorage.getItem('ai-chat-mode');
if (savedMode === 'sidebar') {
enterSidebar();
Expand All @@ -689,6 +756,8 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
popup.style.opacity = '1';
popup.style.transform = 'translateY(0) scale(1)';
}
var savedDraft = sessionStorage.getItem('ai-chat-draft');
if (input && savedDraft) input.value = savedDraft;
})();

// ===========================================================
Expand Down Expand Up @@ -728,9 +797,10 @@ const turnstileSiteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY || '';
// Add user message
addUserMsg(msg);
conversationHistory.push({ role: 'user', content: msg });
saveState();
// Clear before saving so the sent text is not kept as the unsent draft
input.value = '';
input.style.height = 'auto';
saveState();

// Create message group for assistant response
var msgGroup = document.createElement('div');
Expand Down
Loading