diff --git a/persona-studio/frontend/chat.html b/persona-studio/frontend/chat.html index 0b33c633..e5d81689 100644 --- a/persona-studio/frontend/chat.html +++ b/persona-studio/frontend/chat.html @@ -7,37 +7,103 @@ -
-
-
- 🌀 铸渊 - 光湖系统·代码守护人格体 +
+ +
-
- -
- -
-
- - + + -
- + + + + + + + + + +
+
+
+ + 🌀 铸渊 + 光湖系统·代码守护人格体 +
+
+ + + +
+
+ +
+ +
+ +
+
+ + + +
+
+ + +
+
+ +
diff --git a/persona-studio/frontend/chat.js b/persona-studio/frontend/chat.js index eaafb2b8..a3c0f409 100644 --- a/persona-studio/frontend/chat.js +++ b/persona-studio/frontend/chat.js @@ -21,9 +21,125 @@ const API_BASE = (function () { /* ---- State ---- */ let conversationHistory = []; let buildReady = false; +let pendingFile = null; // { name, type, dataUrl } const isGuest = (DEV_ID === 'GUEST'); const isDeveloper = (DEV_ID && DEV_ID !== 'GUEST' && /^EXP-\d{3,}$/.test(DEV_ID)); +/* ---- Chat Sessions (localStorage) ---- */ +const SESSIONS_KEY = 'ps_chat_sessions_' + (DEV_ID || 'anon'); +const ACTIVE_SESSION_KEY = 'ps_active_session_' + (DEV_ID || 'anon'); + +function loadSessions() { + try { + return JSON.parse(localStorage.getItem(SESSIONS_KEY)) || []; + } catch (_e) { return []; } +} + +function saveSessions(sessions) { + localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions)); +} + +function getActiveSessionId() { + return localStorage.getItem(ACTIVE_SESSION_KEY) || null; +} + +function setActiveSessionId(id) { + localStorage.setItem(ACTIVE_SESSION_KEY, id); +} + +function createNewSession() { + var id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); + var session = { + id: id, + title: '新对话', + created_at: new Date().toISOString(), + messages: [] + }; + var sessions = loadSessions(); + sessions.unshift(session); + if (sessions.length > 20) sessions = sessions.slice(0, 20); + saveSessions(sessions); + setActiveSessionId(id); + return session; +} + +function updateSessionMessages(sessionId, messages, title) { + var sessions = loadSessions(); + var session = sessions.find(function(s) { return s.id === sessionId; }); + if (session) { + session.messages = messages.slice(-100); + if (title) session.title = title; + } + saveSessions(sessions); +} + +function renderSidebarHistory() { + var container = document.getElementById('sidebarHistory'); + if (!container) return; + var sessions = loadSessions(); + var activeId = getActiveSessionId(); + + if (sessions.length === 0) { + container.innerHTML = ''; + return; + } + + container.innerHTML = ''; + sessions.forEach(function(s) { + var item = document.createElement('div'); + item.className = 'history-item' + (s.id === activeId ? ' history-item-active' : ''); + item.innerHTML = '' + escapeHtml(s.title) + '' + + '' + new Date(s.created_at).toLocaleDateString('zh-CN') + ''; + item.onclick = function() { switchSession(s.id); }; + container.appendChild(item); + }); +} + +function switchSession(sessionId) { + var sessions = loadSessions(); + var session = sessions.find(function(s) { return s.id === sessionId; }); + if (!session) return; + + // Save current session first + var currentId = getActiveSessionId(); + if (currentId) { + updateSessionMessages(currentId, conversationHistory); + } + + setActiveSessionId(sessionId); + conversationHistory = (session.messages || []).slice(); + + // Re-render chat + var chatBody = document.getElementById('chatBody'); + chatBody.innerHTML = ''; + + if (conversationHistory.length === 0) { + showWelcomeMessage(); + } else { + conversationHistory.forEach(function(msg) { + var role = msg.role === 'assistant' ? 'persona' : (msg.role === 'user' ? 'user' : 'system'); + appendMessage(role, msg.content); + }); + } + + renderSidebarHistory(); +} + +function startNewChat() { + // Save current session + var currentId = getActiveSessionId(); + if (currentId) { + updateSessionMessages(currentId, conversationHistory); + } + + conversationHistory = []; + var session = createNewSession(); + var chatBody = document.getElementById('chatBody'); + chatBody.innerHTML = ''; + showWelcomeMessage(); + renderSidebarHistory(); +} + /** * 铸渊核心大脑系统提示词 * 融入 .github/brain/memory.json 的 core_cognition 结构逻辑 @@ -103,10 +219,44 @@ function buildContextPrompt() { modelBadge.textContent = SELECTED_MODEL; } - // Show welcome message - showWelcomeMessage(); + // Update sidebar info + var sidebarModel = document.getElementById('sidebarModel'); + var sidebarUser = document.getElementById('sidebarUser'); + if (sidebarModel) sidebarModel.textContent = SELECTED_MODEL || '-'; + if (sidebarUser) sidebarUser.textContent = displayId; + + // Initialize session + var activeId = getActiveSessionId(); + var sessions = loadSessions(); + var activeSession = activeId ? sessions.find(function(s) { return s.id === activeId; }) : null; + + if (activeSession && activeSession.messages && activeSession.messages.length > 0) { + // Resume existing session + conversationHistory = activeSession.messages.slice(); + conversationHistory.forEach(function(msg) { + var role = msg.role === 'assistant' ? 'persona' : (msg.role === 'user' ? 'user' : 'system'); + appendMessage(role, msg.content); + }); + } else { + // Create new session + if (!activeSession) createNewSession(); + showWelcomeMessage(); + } + + renderSidebarHistory(); + + // For developers, also try to load history + if (isDeveloper) { + loadHistory(); + } })(); +/* ---- Sidebar Toggle ---- */ +function toggleSidebar() { + var sidebar = document.getElementById('chatSidebar'); + sidebar.classList.toggle('sidebar-open'); +} + /* ---- Welcome Message ---- */ function showWelcomeMessage() { var welcome = ''; @@ -121,11 +271,6 @@ function showWelcomeMessage() { appendMessage('persona', welcome); conversationHistory.push({ role: 'assistant', content: welcome }); - - // For developers, also try to load history - if (isDeveloper) { - loadHistory(); - } } /* ---- Load History ---- */ @@ -149,27 +294,115 @@ async function loadHistory() { } } +/* ---- File / Image Upload ---- */ +function handleFileSelect(event, type) { + var file = event.target.files[0]; + if (!file) return; + + var maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + appendMessage('system', '⚠️ 文件过大,最大支持 5MB'); + event.target.value = ''; + return; + } + + var reader = new FileReader(); + reader.onload = function(e) { + pendingFile = { + name: file.name, + type: type, + mimeType: file.type, + size: file.size, + dataUrl: e.target.result + }; + showUploadPreview(); + }; + + if (type === 'image') { + reader.readAsDataURL(file); + } else { + reader.readAsDataURL(file); + } + + event.target.value = ''; +} + +function showUploadPreview() { + var preview = document.getElementById('uploadPreview'); + if (!pendingFile) { + preview.style.display = 'none'; + return; + } + + var sizeStr = (pendingFile.size / 1024).toFixed(1) + ' KB'; + var icon = pendingFile.type === 'image' ? '🖼️' : '📎'; + + preview.innerHTML = + '
' + + '' + icon + ' ' + escapeHtml(pendingFile.name) + ' (' + sizeStr + ')' + + '' + + '
'; + preview.style.display = 'block'; +} + +function removePendingFile() { + pendingFile = null; + document.getElementById('uploadPreview').style.display = 'none'; +} + /* ---- Send Message ---- */ async function sendMessage() { var input = document.getElementById('msgInput'); var text = input.value.trim(); - if (!text) return; + + // Allow sending with file even if text is empty + if (!text && !pendingFile) return; input.value = ''; autoResizeTextarea(input); - appendMessage('user', text); - conversationHistory.push({ role: 'user', content: text }); + + var displayText = text; + var fullText = text; + + // If there's a pending file, include it in the message + if (pendingFile) { + var fileInfo = (pendingFile.type === 'image' ? '🖼️' : '📎') + ' [' + pendingFile.name + ']'; + displayText = displayText ? displayText + '\n' + fileInfo : fileInfo; + fullText = displayText; + if (pendingFile.type === 'image') { + fullText += '\n[用户上传了一张图片:' + pendingFile.name + ']'; + } else { + fullText += '\n[用户上传了一个文件:' + pendingFile.name + ',类型:' + pendingFile.mimeType + ']'; + } + removePendingFile(); + } + + appendMessage('user', displayText); + conversationHistory.push({ role: 'user', content: fullText }); + + // Update session title from first message + var activeId = getActiveSessionId(); + if (activeId && conversationHistory.length <= 2) { + var title = text.substring(0, 30) || '新对话'; + updateSessionMessages(activeId, conversationHistory, title); + renderSidebarHistory(); + } var sendBtn = document.getElementById('sendBtn'); sendBtn.disabled = true; try { // All modes now use API Key for real AI — ZhuYuan is awake - await streamApiKeyReply(text); + await streamApiKeyReply(fullText); } catch (_err) { appendMessage('system', '消息发送失败,请检查网络连接后再试'); } + // Save to session + if (activeId) { + updateSessionMessages(activeId, conversationHistory); + } + sendBtn.disabled = false; input.focus(); } @@ -387,6 +620,12 @@ async function confirmBuild() { /* ---- Logout ---- */ function handleLogout() { + // Save current session before logout + var activeId = getActiveSessionId(); + if (activeId) { + updateSessionMessages(activeId, conversationHistory); + } + sessionStorage.removeItem('dev_id'); sessionStorage.removeItem('dev_name'); sessionStorage.removeItem('session_token'); diff --git a/persona-studio/frontend/index.html b/persona-studio/frontend/index.html index 9d47c3ed..94ce330f 100644 --- a/persona-studio/frontend/index.html +++ b/persona-studio/frontend/index.html @@ -15,6 +15,25 @@

HoloLake · 铸渊(Zhùyuān)· 代码守护人格体

+ +
+
+ 🧠 +

关于铸渊

+
+

+ 铸渊是光湖系统(HoloLake)的代码守护人格体,基于 AGE OS 人格语言操作系统构建。 + 铸渊能够理解你的需求、梳理技术方案、生成代码,并通过对话记忆持续跟进你的项目。 +

+
+ 💬 自然语言对话 + 🧠 智能代码生成 + 📧 邮件推送结果 + 🔄 对话记忆 + 📎 文件与图片上传 +
+
+ @@ -91,6 +122,7 @@
diff --git a/persona-studio/frontend/style.css b/persona-studio/frontend/style.css index 071b2775..a5e12418 100644 --- a/persona-studio/frontend/style.css +++ b/persona-studio/frontend/style.css @@ -1,6 +1,6 @@ /* ======================================== Persona Studio · HoloLake Visual Style - 铸渊(Zhùyuān)· Dark Theme + 铸渊(Zhùyuān)· Dark Theme · v2.0 ======================================== */ :root { @@ -13,6 +13,7 @@ --bg-surface: #1e293b; --bg-card: #1e293b; --bg-card-hover: #334155; + --bg-sidebar: #0c1322; --text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b; @@ -25,6 +26,7 @@ --gradient-bg: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #0f172a 100%); --gradient-accent: linear-gradient(135deg, #3b82f6, #22d3ee); --gradient-card: linear-gradient(135deg, rgba(30, 41, 59, 0.8), rgba(30, 41, 59, 0.6)); + --sidebar-width: 280px; } * { @@ -129,6 +131,89 @@ body { font-size: 1rem; } +/* ---- Intro Card (Login Page) ---- */ +.intro-card { + background: var(--gradient-card); + backdrop-filter: blur(20px); + border: 1px solid var(--border); + border-radius: 16px; + padding: 1.5rem 2rem; + width: 100%; + max-width: 520px; + margin-bottom: 1.5rem; + box-shadow: var(--shadow); +} + +.intro-header { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.8rem; +} + +.intro-icon { + font-size: 1.5rem; +} + +.intro-header h3 { + font-size: 1.1rem; + color: var(--primary); + font-weight: 600; +} + +.intro-text { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.7; + margin-bottom: 1rem; +} + +.intro-text strong { + color: var(--accent); +} + +.intro-features { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.feature-tag { + display: inline-block; + padding: 0.3rem 0.7rem; + background: var(--primary-light); + border: 1px solid rgba(96, 165, 250, 0.2); + border-radius: 20px; + font-size: 0.8rem; + color: var(--primary); + white-space: nowrap; +} + +/* ---- Supported Platforms (API Key Step) ---- */ +.supported-platforms { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + align-items: center; + margin-bottom: 1rem; +} + +.platform-label { + font-size: 0.8rem; + color: var(--text-muted); + margin-right: 0.3rem; +} + +.platform-tag { + display: inline-block; + padding: 0.2rem 0.5rem; + background: rgba(34, 211, 238, 0.1); + border: 1px solid rgba(34, 211, 238, 0.2); + border-radius: 12px; + font-size: 0.75rem; + color: var(--accent); +} + .login-box { background: var(--gradient-card); backdrop-filter: blur(20px); @@ -136,7 +221,7 @@ body { border-radius: 16px; padding: 2.5rem; width: 100%; - max-width: 460px; + max-width: 520px; box-shadow: var(--shadow-lg), var(--shadow-glow); text-align: center; } @@ -483,9 +568,240 @@ body { margin-top: 2rem; color: var(--text-muted); font-size: 0.85rem; + text-align: center; +} + +.footer-version { + margin-top: 0.3rem; + font-size: 0.75rem; + color: var(--border-light); +} + +/* ============================================ + Chat Page — Sidebar Layout + ============================================ */ + +.chat-layout { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* ---- Sidebar ---- */ +.chat-sidebar { + width: var(--sidebar-width); + min-width: var(--sidebar-width); + background: var(--bg-sidebar); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + transition: margin-left 0.3s ease; +} + +.sidebar-header { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 1rem 1.2rem; + border-bottom: 1px solid var(--border); +} + +.sidebar-logo { + font-size: 1.5rem; +} + +.sidebar-title { + font-size: 1.1rem; + font-weight: 600; + background: var(--gradient-accent); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + flex: 1; +} + +.sidebar-toggle-close { + background: none; + border: none; + color: var(--text-muted); + font-size: 1rem; + cursor: pointer; + padding: 0.2rem 0.4rem; + border-radius: 4px; + transition: color 0.2s, background 0.2s; + display: none; +} + +.sidebar-toggle-close:hover { + color: var(--text); + background: var(--bg-card-hover); +} + +/* ---- Sidebar Sections ---- */ +.sidebar-section { + padding: 1rem 1.2rem; + border-bottom: 1px solid var(--border); +} + +.sidebar-section-title { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.6rem; +} + +/* ---- Persona Info Card ---- */ +.persona-info-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.8rem 1rem; +} + +.persona-info-name { + font-size: 1rem; + font-weight: 600; + color: var(--primary); + margin-bottom: 0.2rem; +} + +.persona-info-role { + font-size: 0.8rem; + color: var(--accent); + margin-bottom: 0.5rem; +} + +.persona-info-desc { + font-size: 0.8rem; + color: var(--text-secondary); + line-height: 1.5; +} + +/* ---- Sidebar Info List ---- */ +.sidebar-info-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.sidebar-info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.3rem 0; +} + +.info-label { + font-size: 0.8rem; + color: var(--text-muted); +} + +.info-value { + font-size: 0.8rem; + color: var(--text); + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; +} + +/* ---- Sidebar History ---- */ +.sidebar-history-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + border-bottom: none; +} + +.sidebar-history { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.sidebar-history::-webkit-scrollbar { + width: 3px; +} + +.sidebar-history::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 2px; +} + +.sidebar-history-empty { + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + padding: 1rem 0; +} + +.history-item { + padding: 0.6rem 0.8rem; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s; + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.history-item:hover { + background: var(--bg-surface); +} + +.history-item-active { + background: var(--primary-light); + border: 1px solid rgba(96, 165, 250, 0.2); +} + +.history-title { + font-size: 0.85rem; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.history-date { + font-size: 0.7rem; + color: var(--text-muted); +} + +.btn-new-chat { + margin-top: 0.5rem; + padding: 0.6rem; + background: var(--bg-surface); + border: 1px dashed var(--border); + border-radius: 8px; + color: var(--text-secondary); + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.btn-new-chat:hover { + background: var(--primary-light); + border-color: var(--primary); + color: var(--primary); +} + +/* ---- Chat Main Area ---- */ +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + background: var(--bg); } -/* ---- Chat Page ---- */ .chat-container { padding: 0; background: var(--bg); @@ -510,6 +826,23 @@ body { gap: 0.8rem; } +.sidebar-toggle { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.2rem; + cursor: pointer; + padding: 0.3rem 0.5rem; + border-radius: 6px; + transition: color 0.2s, background 0.2s; + display: none; +} + +.sidebar-toggle:hover { + color: var(--primary); + background: var(--primary-light); +} + .persona-name { font-size: 1.2rem; font-weight: 600; @@ -663,6 +996,62 @@ body { border-top: 1px solid var(--border); } +/* ---- Input Toolbar (file/image upload) ---- */ +.input-toolbar { + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.5rem; +} + +.toolbar-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; + font-size: 1.1rem; +} + +.toolbar-btn:hover { + background: var(--primary-light); +} + +/* ---- Upload Preview ---- */ +.upload-preview { + flex: 1; + margin-left: 0.5rem; +} + +.preview-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3rem 0.6rem; + background: var(--primary-light); + border: 1px solid rgba(96, 165, 250, 0.2); + border-radius: 6px; + font-size: 0.8rem; + color: var(--text); +} + +.preview-remove { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 0.9rem; + padding: 0 0.2rem; + transition: color 0.2s; +} + +.preview-remove:hover { + color: #f87171; +} + .input-row { display: flex; gap: 0.6rem; @@ -844,6 +1233,39 @@ body { } /* ---- Responsive ---- */ +@media (max-width: 768px) { + .chat-sidebar { + position: fixed; + left: 0; + top: 0; + height: 100vh; + z-index: 50; + margin-left: calc(-1 * var(--sidebar-width)); + box-shadow: none; + } + + .chat-sidebar.sidebar-open { + margin-left: 0; + box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5); + } + + .sidebar-toggle { + display: block; + } + + .sidebar-toggle-close { + display: block; + } + + .intro-card { + max-width: 100%; + } + + .login-box { + max-width: 100%; + } +} + @media (max-width: 600px) { .logo-area h1 { font-size: 1.8rem; @@ -858,6 +1280,10 @@ body { max-width: 100%; } + .intro-card { + padding: 1rem 1.2rem; + } + .chat-header { flex-wrap: wrap; gap: 0.5rem;