Bladeren bron

styles change

sequoia00 5 dagen geleden
bovenliggende
commit
01d1614483
4 gewijzigde bestanden met toevoegingen van 236 en 13 verwijderingen
  1. 4 3
      chatfast/config.py
  2. 169 7
      static/app.js
  3. 2 1
      static/index.html
  4. 61 2
      static/styles.css

+ 4 - 3
chatfast/config.py

@@ -36,14 +36,15 @@ DOWNLOAD_BASE = DEFAULT_UPLOAD_BASE.rstrip("/") if DEFAULT_UPLOAD_BASE else ""
 default_key = "sk-re2NlaKIQn11ZNWzAbB6339cEbF94c6aAfC8B7Ab82879bEa"
 MODEL_KEYS = {
     "grok-3": default_key,
-    "grok-4": default_key,
-    "gpt-5.1-2025-11-13": default_key,
-    "gpt-5-2025-08-07": default_key,
+    "grok-4.1": default_key,
+    "gpt-5.1-all": default_key,
     "gpt-4o-mini": default_key,
     "o1-mini": default_key,
     "o4-mini": default_key,
     "deepseek-v3": default_key,
     "deepseek-r1": default_key,
+    "grok-4": default_key,
+    "gpt-5-all": default_key,
     "gpt-4o-all": default_key,
     "o3-mini-all": default_key,
 }

+ 169 - 7
static/app.js

@@ -89,6 +89,7 @@
         dom.chatInput = document.getElementById('chat-input');
         dom.sendButton = document.getElementById('send-btn');
         dom.fileInput = document.getElementById('file-input');
+        dom.expandAllButton = document.getElementById('expand-all-btn');
         dom.chatStatus = document.getElementById('chat-status');
         dom.sessionIndicator = document.getElementById('session-indicator');
         dom.userMenu = document.getElementById('user-menu');
@@ -222,6 +223,15 @@
         });
 
         dom.chatForm.addEventListener('submit', handleSubmitMessage);
+        if (dom.expandAllButton) {
+            dom.expandAllButton.addEventListener('click', () => {
+                if (!state.token || !Array.isArray(state.messages) || !state.messages.length) {
+                    return;
+                }
+                state.expandedMessages = new Set(state.messages.map((_, idx) => idx));
+                renderMessages({ preserveScroll: true });
+            });
+        }
 
         dom.chatInput.addEventListener('keydown', (event) => {
             if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.altKey) {
@@ -985,7 +995,7 @@
         if (!dom.chatStatus) {
             return;
         }
-        dom.chatStatus.textContent = message || '';
+        dom.chatStatus.innerHTML = '';
         dom.chatStatus.classList.remove('running', 'error');
         if (!message) {
             return;
@@ -993,6 +1003,16 @@
         if (stateClass) {
             dom.chatStatus.classList.add(stateClass);
         }
+        if (stateClass === 'running') {
+            const spinner = document.createElement('span');
+            spinner.className = 'chat-status-spinner';
+            spinner.setAttribute('aria-hidden', 'true');
+            dom.chatStatus.appendChild(spinner);
+        }
+        const textEl = document.createElement('span');
+        textEl.className = 'chat-status-text';
+        textEl.textContent = message;
+        dom.chatStatus.appendChild(textEl);
     }
 
     function setStreaming(active) {
@@ -1165,10 +1185,12 @@
         }
     }
 
-    function renderMessages() {
+    function renderMessages(options = {}) {
         if (!dom.chatMessages) {
             return;
         }
+        const { preserveScroll = false } = options;
+        const previousScrollTop = preserveScroll ? dom.chatMessages.scrollTop : 0;
         dom.chatMessages.innerHTML = '';
         if (!state.token) {
             const notice = document.createElement('div');
@@ -1217,7 +1239,7 @@
                     } else {
                         state.expandedMessages.add(index);
                     }
-                    renderMessages();
+                    renderMessages({ preserveScroll: true });
                 });
                 actions.appendChild(toggleButton);
             }
@@ -1249,7 +1271,11 @@
         });
 
         updateSearchFeedback();
-        scrollToBottom();
+        if (preserveScroll) {
+            dom.chatMessages.scrollTop = previousScrollTop;
+        } else {
+            scrollToBottom();
+        }
     }
 
     function renderContent(content, container, query) {
@@ -1297,6 +1323,7 @@
         let paragraphBuffer = [];
         let listBuffer = [];
         let blockquoteBuffer = [];
+        let tableBuffer = null;
         let inCodeBlock = false;
         let codeLang = '';
         let codeBuffer = [];
@@ -1337,7 +1364,66 @@
             blockquoteBuffer = [];
         };
 
+        const parseTableRow = (line) => line
+            .trim()
+            .replace(/^\|/, '')
+            .replace(/\|$/, '')
+            .split('|')
+            .map((cell) => cell.trim());
+
+        const flushTable = () => {
+            if (!tableBuffer || tableBuffer.length < 2) {
+                if (tableBuffer && tableBuffer.length) {
+                    tableBuffer.forEach((line) => paragraphBuffer.push(line));
+                }
+                tableBuffer = null;
+                return;
+            }
+            const headerCells = parseTableRow(tableBuffer[0]);
+            const dividerCells = parseTableRow(tableBuffer[1]);
+            const isDividerValid = dividerCells.length === headerCells.length
+                && dividerCells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
+            if (!isDividerValid) {
+                tableBuffer.forEach((line) => paragraphBuffer.push(line));
+                tableBuffer = null;
+                return;
+            }
+            const table = document.createElement('table');
+            table.className = 'md-table';
+            const thead = document.createElement('thead');
+            const headRow = document.createElement('tr');
+            headerCells.forEach((cell) => {
+                const th = document.createElement('th');
+                appendInlineMarkdown(th, cell);
+                headRow.appendChild(th);
+            });
+            thead.appendChild(headRow);
+            table.appendChild(thead);
+
+            if (tableBuffer.length > 2) {
+                const tbody = document.createElement('tbody');
+                for (let i = 2; i < tableBuffer.length; i += 1) {
+                    const rowCells = parseTableRow(tableBuffer[i]);
+                    if (rowCells.length === 1 && !rowCells[0]) {
+                        continue;
+                    }
+                    const row = document.createElement('tr');
+                    rowCells.forEach((cell) => {
+                        const td = document.createElement('td');
+                        appendInlineMarkdown(td, cell);
+                        row.appendChild(td);
+                    });
+                    tbody.appendChild(row);
+                }
+                table.appendChild(tbody);
+            }
+            container.appendChild(table);
+            tableBuffer = null;
+        };
+
         const flushCode = () => {
+            const wrapper = document.createElement('div');
+            wrapper.className = 'code-block';
             const pre = document.createElement('pre');
             const code = document.createElement('code');
             if (codeLang) {
@@ -1346,7 +1432,10 @@
             }
             code.textContent = codeBuffer.join('\n');
             pre.appendChild(code);
-            container.appendChild(pre);
+            const copyButton = createCopyButton(code);
+            wrapper.appendChild(copyButton);
+            wrapper.appendChild(pre);
+            container.appendChild(wrapper);
             codeBuffer = [];
             codeLang = '';
             inCodeBlock = false;
@@ -1354,7 +1443,8 @@
 
         lines.forEach((rawLine) => {
             const line = rawLine;
-            const fenceMatch = line.match(/^```([A-Za-z0-9_-]+)?\s*$/);
+            const trimmedLine = rawLine.trim();
+            const fenceMatch = trimmedLine.match(/^```([\w+-]+)?\s*$/);
             if (fenceMatch) {
                 if (inCodeBlock) {
                     flushCode();
@@ -1374,6 +1464,21 @@
                 return;
             }
 
+            const isTableRowLine = /^\|.+\|.*$/.test(trimmedLine);
+            if (isTableRowLine) {
+                if (!tableBuffer) {
+                    flushParagraph();
+                    flushList();
+                    flushBlockquote();
+                    tableBuffer = [];
+                }
+                tableBuffer.push(trimmedLine);
+                return;
+            }
+            if (tableBuffer) {
+                flushTable();
+            }
+
             const listMatch = line.match(/^\s*[-*+]\s+(.*)$/);
             if (listMatch) {
                 flushParagraph();
@@ -1390,10 +1495,11 @@
                 return;
             }
 
-            if (!line.trim()) {
+            if (!trimmedLine.length) {
                 flushParagraph();
                 flushList();
                 flushBlockquote();
+                flushTable();
                 return;
             }
 
@@ -1418,6 +1524,7 @@
         flushParagraph();
         flushList();
         flushBlockquote();
+        flushTable();
     }
 
     function appendInlineMarkdown(parent, text) {
@@ -1501,6 +1608,61 @@
         });
     }
 
+    function createCopyButton(codeElement) {
+        const button = document.createElement('button');
+        button.type = 'button';
+        button.className = 'code-copy-btn';
+        button.textContent = '复制';
+        button.title = '复制代码';
+        button.setAttribute('aria-label', '复制代码');
+        button.addEventListener('click', async () => {
+            const text = codeElement && codeElement.textContent ? codeElement.textContent : '';
+            if (!text) {
+                return;
+            }
+            const defaultLabel = button.textContent;
+            button.disabled = true;
+            try {
+                await copyTextToClipboard(text);
+                button.textContent = '已复制';
+            } catch (err) {
+                console.error('复制失败', err);
+                button.textContent = '复制失败';
+            } finally {
+                setTimeout(() => {
+                    button.textContent = defaultLabel;
+                    button.disabled = false;
+                }, 1500);
+            }
+        });
+        return button;
+    }
+
+    async function copyTextToClipboard(text) {
+        if (!text) {
+            return;
+        }
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+            await navigator.clipboard.writeText(text);
+            return;
+        }
+        const textarea = document.createElement('textarea');
+        textarea.value = text;
+        textarea.setAttribute('readonly', 'true');
+        textarea.style.position = 'absolute';
+        textarea.style.left = '-9999px';
+        document.body.appendChild(textarea);
+        textarea.select();
+        try {
+            const success = document.execCommand('copy');
+            if (!success) {
+                throw new Error('复制失败');
+            }
+        } finally {
+            document.body.removeChild(textarea);
+        }
+    }
+
     function clearHighlights(root) {
         if (!root) {
             return;

+ 2 - 1
static/index.html

@@ -9,7 +9,7 @@
 <body>
     <div id="auth-view" class="auth-view">
         <div class="auth-card">
-            <h1>欢迎使用聊天系统</h1>
+            <h1>欢迎使用i满意聊天系统</h1>
             <p class="auth-hint">系统分为管理员与普通用户,默认管理员:admin / Admin@123</p>
             <form id="login-form" class="auth-form">
                 <label>用户名
@@ -106,6 +106,7 @@
                 <textarea id="chat-input" placeholder="What is up?" rows="3" class="chat-textarea"></textarea>
                 <div class="chat-form-footer">
                     <label for="file-input" class="file-input-label">选择文件</label>
+                    <button type="button" id="expand-all-btn" class="secondary-button">全部展开</button>
                     <input id="file-input" type="file" multiple accept=".jpg,.jpeg,.png,.txt,.pdf,.doc,.docx" class="file-input">
                     <button type="submit" id="send-btn" class="primary-button">发送</button>
                     <button type="button" id="end-chat-btn" class="secondary-button danger hidden" title="提前结束此次对话">提前结束</button>

+ 61 - 2
static/styles.css

@@ -189,6 +189,7 @@
     text-align: left;
     border-radius: 8px;
     font-size: 14px;
+    color:black;
     cursor: pointer;
 }
 
@@ -709,6 +710,27 @@ body {
     color: var(--secondary-text);
 }
 
+.message-content table.md-table {
+    width: 100%;
+    border-collapse: collapse;
+    margin: 12px 0;
+    font-size: 14px;
+    background: #fff;
+}
+
+.message-content table.md-table th,
+.message-content table.md-table td {
+    border: 1px solid var(--border-color);
+    padding: 8px 10px;
+    text-align: left;
+    vertical-align: top;
+}
+
+.message-content table.md-table thead {
+    background: #f5f6f8;
+    font-weight: 600;
+}
+
 .message-content h1,
 .message-content h2,
 .message-content h3,
@@ -738,6 +760,39 @@ body {
     margin: 10px 0;
 }
 
+.code-block {
+    position: relative;
+    margin: 10px 0;
+}
+
+.code-block pre {
+    margin: 0;
+    padding-right: 48px;
+}
+
+.code-copy-btn {
+    position: absolute;
+    top: 10px;
+    right: 12px;
+    border: 1px solid rgba(255, 255, 255, 0.25);
+    background: rgba(255, 255, 255, 0.12);
+    color: #fff;
+    border-radius: 6px;
+    padding: 2px 10px;
+    font-size: 12px;
+    cursor: pointer;
+    transition: background 0.2s ease, color 0.2s ease;
+}
+
+.code-copy-btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+}
+
+.code-copy-btn:not(:disabled):hover {
+    background: rgba(255, 255, 255, 0.2);
+}
+
 .message-content code {
     font-family: "JetBrains Mono", "Fira Code", "Menlo", "Consolas", monospace;
     background: rgba(13, 110, 253, 0.12);
@@ -844,8 +899,7 @@ body {
     color: var(--accent);
 }
 
-.chat-status.running::before {
-    content: "";
+.chat-status-spinner {
     width: 14px;
     height: 14px;
     border-radius: 50%;
@@ -854,6 +908,11 @@ body {
     animation: chat-spin 0.8s linear infinite;
 }
 
+.chat-status-text {
+    display: inline-flex;
+    align-items: center;
+}
+
 .chat-status.error {
     color: #dc3545;
 }