Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Deep Research</title> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| color: #18212f; | |
| background: #f5f7fa; | |
| } | |
| .shell { | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| min-height: 100vh; | |
| } | |
| .topbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| padding: 18px 24px; | |
| border-bottom: 1px solid #d9e0e9; | |
| background: #ffffff; | |
| } | |
| .brand h1 { | |
| margin: 0; | |
| font-size: 20px; | |
| line-height: 1.2; | |
| } | |
| .brand p { | |
| margin: 3px 0 0; | |
| color: #657184; | |
| font-size: 13px; | |
| } | |
| .mode-toggle { | |
| display: inline-grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| padding: 3px; | |
| border: 1px solid #ccd5e1; | |
| border-radius: 8px; | |
| background: #eef2f6; | |
| } | |
| .mode-toggle button { | |
| min-width: 78px; | |
| border: 0; | |
| border-radius: 6px; | |
| padding: 8px 10px; | |
| background: transparent; | |
| color: #4b586a; | |
| font: inherit; | |
| font-size: 13px; | |
| cursor: pointer; | |
| } | |
| .mode-toggle button.active { | |
| background: #ffffff; | |
| color: #0f1724; | |
| box-shadow: 0 1px 3px rgba(15, 23, 36, 0.12); | |
| } | |
| .chat { | |
| width: min(980px, 100%); | |
| margin: 0 auto; | |
| padding: 24px; | |
| overflow-y: auto; | |
| } | |
| .empty { | |
| display: grid; | |
| align-content: center; | |
| min-height: 52vh; | |
| color: #657184; | |
| } | |
| .empty h2 { | |
| margin: 0 0 8px; | |
| color: #18212f; | |
| font-size: clamp(28px, 5vw, 48px); | |
| letter-spacing: 0; | |
| } | |
| .empty p { | |
| max-width: 620px; | |
| margin: 0; | |
| line-height: 1.6; | |
| } | |
| .message { | |
| width: fit-content; | |
| max-width: min(760px, 100%); | |
| margin: 0 0 14px; | |
| padding: 13px 15px; | |
| border-radius: 8px; | |
| line-height: 1.55; | |
| white-space: pre-wrap; | |
| } | |
| .message.user { | |
| margin-left: auto; | |
| background: #1f6feb; | |
| color: #ffffff; | |
| } | |
| .message.assistant { | |
| background: #ffffff; | |
| border: 1px solid #d9e0e9; | |
| } | |
| .message.progress { | |
| background: #fff8e6; | |
| border: 1px solid #efd18a; | |
| color: #705400; | |
| } | |
| .message h1, | |
| .message h2, | |
| .message h3 { | |
| margin: 10px 0 8px; | |
| font-size: 18px; | |
| } | |
| .message p { | |
| margin: 0 0 10px; | |
| } | |
| .message ul, | |
| .message ol { | |
| margin: 0 0 10px 22px; | |
| padding: 0; | |
| } | |
| .composer { | |
| border-top: 1px solid #d9e0e9; | |
| background: #ffffff; | |
| padding: 16px 24px 22px; | |
| } | |
| .composer form { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| gap: 10px; | |
| width: min(980px, 100%); | |
| margin: 0 auto; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 48px; | |
| max-height: 160px; | |
| resize: vertical; | |
| border: 1px solid #c6d0dc; | |
| border-radius: 8px; | |
| padding: 13px 14px; | |
| color: #18212f; | |
| font: inherit; | |
| line-height: 1.4; | |
| outline: none; | |
| } | |
| textarea:focus { | |
| border-color: #1f6feb; | |
| box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.14); | |
| } | |
| .send { | |
| width: 48px; | |
| height: 48px; | |
| border: 0; | |
| border-radius: 8px; | |
| background: #1f6feb; | |
| color: #ffffff; | |
| font-size: 20px; | |
| cursor: pointer; | |
| } | |
| .send:disabled { | |
| opacity: 0.55; | |
| cursor: not-allowed; | |
| } | |
| @media (max-width: 720px) { | |
| .topbar { | |
| align-items: stretch; | |
| flex-direction: column; | |
| } | |
| .mode-toggle { | |
| width: 100%; | |
| } | |
| .mode-toggle button { | |
| min-width: 0; | |
| } | |
| .chat, | |
| .composer { | |
| padding-left: 14px; | |
| padding-right: 14px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="shell"> | |
| <header class="topbar"> | |
| <div class="brand"> | |
| <h1>Deep Research</h1> | |
| <p>Chat normally, or ask for sourced research when you need depth.</p> | |
| </div> | |
| <div class="mode-toggle" aria-label="Response mode"> | |
| <button type="button" class="active" data-mode="auto">Auto</button> | |
| <button type="button" data-mode="chat">Chat</button> | |
| <button type="button" data-mode="research">Research</button> | |
| </div> | |
| </header> | |
| <main class="chat" id="chat"> | |
| <section class="empty" id="empty"> | |
| <h2>Ask anything.</h2> | |
| <p>Quick questions stay in normal chat. Requests for research, current sources, reports, literature reviews, or market analysis trigger the deeper workflow.</p> | |
| </section> | |
| </main> | |
| <footer class="composer"> | |
| <form id="form"> | |
| <textarea id="input" placeholder="Ask a quick question, or request deep research..." required></textarea> | |
| <button class="send" id="send" type="submit" aria-label="Send">→</button> | |
| </form> | |
| </footer> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <script> | |
| const chat = document.getElementById("chat"); | |
| const empty = document.getElementById("empty"); | |
| const form = document.getElementById("form"); | |
| const input = document.getElementById("input"); | |
| const send = document.getElementById("send"); | |
| const modeButtons = [...document.querySelectorAll("[data-mode]")]; | |
| const sessionId = crypto.randomUUID(); | |
| let mode = "auto"; | |
| modeButtons.forEach((button) => { | |
| button.addEventListener("click", () => { | |
| mode = button.dataset.mode; | |
| modeButtons.forEach((item) => item.classList.toggle("active", item === button)); | |
| input.focus(); | |
| }); | |
| }); | |
| function addMessage(content, type, markdown = false) { | |
| empty?.remove(); | |
| const node = document.createElement("article"); | |
| node.className = `message ${type}`; | |
| node.innerHTML = markdown && window.marked ? marked.parse(content) : escapeHtml(content); | |
| chat.appendChild(node); | |
| chat.scrollTop = chat.scrollHeight; | |
| return node; | |
| } | |
| function escapeHtml(value) { | |
| const div = document.createElement("div"); | |
| div.textContent = value; | |
| return div.innerHTML; | |
| } | |
| function setLoading(value) { | |
| send.disabled = value; | |
| input.disabled = value; | |
| send.textContent = value ? "..." : "→"; | |
| } | |
| form.addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| const topic = input.value.trim(); | |
| if (!topic) return; | |
| addMessage(topic, "user"); | |
| input.value = ""; | |
| setLoading(true); | |
| try { | |
| const response = await fetch("/research", { | |
| method: "POST", | |
| headers: {"Content-Type": "application/json"}, | |
| body: JSON.stringify({topic, session_id: sessionId, mode}), | |
| }); | |
| if (!response.ok) { | |
| const error = await response.text(); | |
| throw new Error(error || `Request failed with ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| while (true) { | |
| const {value, done} = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, {stream: true}); | |
| const events = buffer.split("\n\n"); | |
| buffer = events.pop(); | |
| for (const eventText of events) { | |
| const line = eventText.split("\n").find((item) => item.startsWith("data: ")); | |
| if (!line) continue; | |
| const data = JSON.parse(line.slice(6)); | |
| if (data.status === "progress") { | |
| addMessage(data.message, "progress"); | |
| } | |
| if (data.status === "complete") { | |
| addMessage(data.report, "assistant", true); | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| addMessage(`Error: ${error.message}`, "progress"); | |
| } finally { | |
| setLoading(false); | |
| input.focus(); | |
| } | |
| }); | |
| input.addEventListener("keydown", (event) => { | |
| if (event.key === "Enter" && !event.shiftKey) { | |
| event.preventDefault(); | |
| form.requestSubmit(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |