readysite / website / views / layouts / admin-chat.html
14.1 KB
admin-chat.html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{block "title" .}}Workspace{{end}} - {{dashboard.SiteName}}</title>
    <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" crossorigin="anonymous" />
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/htmx-ext-sse@2.2.2/sse.min.js" integrity="sha384-BkCCd4DbvhfvRLfThp+5RN2KiB1FBROoAtwZFfceJishUnpFW1eF/aqG14M5TwA2" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js" integrity="sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js" integrity="sha384-vdScihEZCfbPnBQf+lc7LgXUdJVYyhC3yWHUW5C5P5GpHRqVnaM6HJELJxT6IqwM" crossorigin="anonymous"></script>
    <style>
        /* Custom scrollbar for chat messages */
        .chat-messages::-webkit-scrollbar {
            width: 6px;
        }
        .chat-messages::-webkit-scrollbar-track {
            background: transparent;
        }
        .chat-messages::-webkit-scrollbar-thumb {
            background: oklch(var(--bc) / 0.2);
            border-radius: 3px;
        }
        .chat-messages::-webkit-scrollbar-thumb:hover {
            background: oklch(var(--bc) / 0.3);
        }
        /* Chat send button loading state */
        .chat-send-loading { display: none; }
        .chat-form.htmx-request .chat-send-icon { display: none; }
        .chat-form.htmx-request .chat-send-loading { display: inline-flex; }
        /* Prevent body scroll */
        body {
            overflow: hidden;
        }
        /* Code preview styling */
        .code-preview {
            font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
            font-size: 13px;
            line-height: 1.5;
            tab-size: 2;
        }
        /* AI response prose styling */
        .prose p {
            margin-bottom: 0.75em;
        }
        .prose p:last-child {
            margin-bottom: 0;
        }
        .prose ul, .prose ol {
            margin-top: 0.5em;
            margin-bottom: 0.75em;
        }
        .prose li {
            margin-bottom: 0.25em;
        }
        .prose code {
            background: oklch(var(--b3));
            padding: 0.125em 0.25em;
            border-radius: 0.25em;
            font-size: 0.875em;
        }
        .prose pre {
            background: oklch(var(--b3));
            padding: 0.75em;
            border-radius: 0.5em;
            overflow-x: auto;
            margin: 0.75em 0;
        }
        .prose pre code {
            background: none;
            padding: 0;
        }
        /* Desktop (lg+): inline panel, hidden via display:none */
        @media (min-width: 1024px) {
            #chat-panel[data-state="closed"] {
                display: none;
            }
        }
        /* Mobile (<lg): fixed drawer with slide animation */
        @media (max-width: 1023px) {
            #chat-panel {
                position: fixed;
                top: 0;
                right: 0;
                bottom: 0;
                z-index: 40;
                transform: translateX(100%);
                transition: transform 0.3s ease-in-out;
            }
            #chat-panel[data-state="open"] {
                transform: translateX(0);
            }
        }
    </style>
</head>
<body class="h-screen bg-base-200">
    <div class="flex h-screen">
        <!-- Slim Sidebar -->
        {{template "admin-nav-slim.html" .}}

        <!-- Main Content Area -->
        <div class="flex flex-1 min-w-0">
            <!-- Preview Panel (flex-1, targeted swap destination) -->
            <div id="preview-content" class="flex-1 flex flex-col min-w-0 bg-base-100"
                 hx-boost="true" hx-target="#preview-content" hx-select="#preview-content" hx-swap="outerHTML">
                {{block "preview" .}}{{end}}
            </div>

            <!-- Mobile overlay -->
            <div id="chat-overlay" class="fixed inset-0 bg-black/50 z-30 hidden" onclick="toggleChat()"></div>

            <!-- Chat Panel (inline on desktop, drawer on mobile) -->
            <div id="chat-panel" data-state="closed" hx-preserve="true"
                 class="w-[320px] lg:w-[420px] flex-shrink-0 flex flex-col border-l border-base-300 bg-base-100">
                {{block "chat" .}}{{end}}
            </div>
        </div>
    </div>

    <!-- Modal Container -->
    <div id="modal-container"></div>

    <!-- Toast Notifications -->
    <div id="toast-container" class="toast toast-end toast-bottom z-[100]"></div>

    <!-- Mobile FAB for chat -->
    <button id="chat-fab" onclick="toggleChat()" class="lg:hidden fixed bottom-6 right-6 btn btn-primary btn-circle shadow-xl z-50">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
        </svg>
    </button>

    <script>
        // Apply saved chat state on page load
        function applyChatState() {
            var panel = document.getElementById('chat-panel');
            if (!panel) return;
            var saved = localStorage.getItem('readysite-chat-open');
            panel.dataset.state = saved === '1' ? 'open' : 'closed';
            updateMobileUI(saved === '1');
        }

        // Toggle chat panel open/closed
        function toggleChat() {
            var panel = document.getElementById('chat-panel');
            if (!panel) return;
            var isOpen = panel.dataset.state === 'open';
            panel.dataset.state = isOpen ? 'closed' : 'open';
            localStorage.setItem('readysite-chat-open', isOpen ? '' : '1');
            updateMobileUI(!isOpen);
            if (!isOpen) {
                setTimeout(function() {
                    var input = panel.querySelector('textarea[name=content]');
                    if (input) input.focus();
                }, 300);
            }
        }

        // Update mobile-specific UI (overlay and FAB)
        function updateMobileUI(isOpen) {
            var overlay = document.getElementById('chat-overlay');
            var fab = document.getElementById('chat-fab');
            if (window.innerWidth < 1024) {
                if (overlay) overlay.classList.toggle('hidden', !isOpen);
                if (fab) fab.classList.toggle('hidden', isOpen);
            }
        }

        // Focus chat input (opens panel if closed)
        function focusChatInput() {
            var panel = document.getElementById('chat-panel');
            if (!panel) return;
            if (panel.dataset.state !== 'open') {
                toggleChat();
            }
            setTimeout(function() {
                var input = panel.querySelector('textarea[name=content]');
                if (input) input.focus();
            }, 100);
        }

        // Navigate preview panel (used by AI navigate_user tool)
        function navigatePreview(url) {
            htmx.ajax('GET', url, {
                target: '#preview-content',
                swap: 'outerHTML',
                select: '#preview-content'
            });
            history.pushState({}, '', url);
            updateActiveNav(url);
        }

        // Update sidebar active nav after targeted swap
        function updateActiveNav(url) {
            var path = url || window.location.pathname;
            document.querySelectorAll('aside nav a, aside .flex a').forEach(function(a) {
                var href = a.getAttribute('href');
                if (!href) return;
                var isActive = (href === '/admin' && path === '/admin') ||
                               (href !== '/admin' && path.startsWith(href));
                var btn = a;
                btn.className = btn.className
                    .replace(/bg-primary\/20 text-primary/g, '')
                    .replace(/hover:bg-base-200\/80/g, '');
                if (isActive) {
                    btn.className += ' bg-primary/20 text-primary';
                } else {
                    btn.className += ' hover:bg-base-200/80';
                }
            });
        }

        // Handle tab switching for preview
        function switchTab(tabName, container) {
            if (!container) return;

            // Update active tab buttons
            container.querySelectorAll('[data-tab]').forEach(function(t) {
                t.classList.toggle('tab-active', t.dataset.tab === tabName);
            });

            // Show/hide content
            container.querySelectorAll('[data-tab-content]').forEach(function(content) {
                content.classList.toggle('hidden', content.dataset.tabContent !== tabName);
            });

            // Update URL query param
            var url = new URL(window.location);
            if (tabName && tabName !== 'preview') {
                url.searchParams.set('tab', tabName);
            } else {
                url.searchParams.delete('tab');
            }
            history.replaceState({}, '', url);
        }

        // Restore tab from URL on page load
        function restoreTabFromURL() {
            var url = new URL(window.location);
            var tabName = url.searchParams.get('tab');
            if (!tabName) return;

            var tabBtn = document.querySelector('[data-tab="' + tabName + '"]');
            if (!tabBtn) return;

            var container = tabBtn.parentElement;
            while (container && !container.querySelector('[data-tab-content]')) {
                container = container.parentElement;
            }
            if (container) {
                switchTab(tabName, container);
            }
        }

        document.addEventListener('click', function(e) {
            const tab = e.target.closest('[data-tab]');
            if (!tab) return;

            const tabName = tab.dataset.tab;

            // Find the nearest ancestor that contains tab-content elements
            let container = tab.parentElement;
            while (container && !container.querySelector('[data-tab-content]')) {
                container = container.parentElement;
            }

            switchTab(tabName, container);
        });

        // Keyboard shortcuts
        document.addEventListener('keydown', function(e) {
            // Cmd/Ctrl + K to focus chat
            if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
                e.preventDefault();
                focusChatInput();
            }
            // Escape to close chat on mobile
            if (e.key === 'Escape') {
                var panel = document.getElementById('chat-panel');
                if (panel && panel.dataset.state === 'open' && window.innerWidth < 1024) {
                    toggleChat();
                }
            }
        });

        // Auto-scroll chat to bottom
        function scrollChatToBottom() {
            const messages = document.querySelector('.chat-messages');
            if (messages) {
                messages.scrollTop = messages.scrollHeight;
            }
        }

        // Render markdown in AI response (sanitized)
        function renderMarkdown(el) {
            if (!el || !window.marked) return;
            var text = el.textContent || '';
            if (text.trim()) {
                var html = marked.parse(text);
                el.innerHTML = window.DOMPurify ? DOMPurify.sanitize(html) : html;
            }
        }

        // Render all markdown in chat messages on load
        function renderAllMarkdown() {
            document.querySelectorAll('.ai-markdown').forEach(renderMarkdown);
        }

        // Apply state on load
        document.addEventListener('DOMContentLoaded', function() {
            applyChatState();
            renderAllMarkdown();
            scrollChatToBottom();
            restoreTabFromURL();
        });

        // After HTMX settles: update nav + scroll chat + restore tab
        document.addEventListener('htmx:afterSettle', function(e) {
            var target = e.detail.target;
            if (target && target.id === 'preview-content') {
                updateActiveNav();
                restoreTabFromURL();
            }
            if (target === document.body || target.closest('.chat-messages') || target.classList.contains('chat-messages')) {
                scrollChatToBottom();
            }
        });

        // Auto-scroll during SSE streaming
        document.addEventListener('htmx:sseMessage', function() {
            scrollChatToBottom();
        });

        // Toast notifications via HX-Trigger
        document.addEventListener('showToast', function(e) {
            var container = document.getElementById('toast-container');
            var toast = document.createElement('div');
            toast.className = 'alert alert-success shadow-lg';
            toast.innerHTML = '<span>' + e.detail.value + '</span>';
            container.appendChild(toast);
            setTimeout(function() {
                toast.style.transition = 'opacity 0.3s';
                toast.style.opacity = '0';
                setTimeout(function() { toast.remove(); }, 300);
            }, 3000);
        });
    </script>
</body>
</html>

{{define "ai-panel-trigger"}}
<button onclick="toggleChat()" class="btn btn-ghost btn-sm btn-square" title="AI Assistant (Ctrl+K)">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
    </svg>
</button>
{{end}}
← Back