| | import React, { useState, useEffect, useMemo, useRef } from 'react'; |
| | import './App.css'; |
| | import DOMPurify from 'dompurify'; |
| | import { marked } from 'marked'; |
| |
|
| | |
| | function useDebounce(value, delay) { |
| | const [debouncedValue, setDebouncedValue] = useState(value); |
| |
|
| | React.useEffect(() => { |
| | const handler = setTimeout(() => { |
| | setDebouncedValue(value); |
| | }, delay); |
| |
|
| | return () => clearTimeout(handler); |
| | }, [value, delay]); |
| |
|
| | return debouncedValue; |
| | } |
| |
|
| | function MarkdownEditor({ value }) { |
| | const containerRef = useRef(null); |
| |
|
| | const htmlContent = marked(value || ''); |
| | const sanitizedHtml = DOMPurify.sanitize(htmlContent); |
| |
|
| | const [userScrolled, setUserScrolled] = useState(false); |
| |
|
| | useEffect(() => { |
| | const container = containerRef.current; |
| | if (container && !userScrolled) { |
| | requestAnimationFrame(() => { |
| | container.scrollTop = container.scrollHeight; |
| | }); |
| | } |
| | }, [value, userScrolled]); |
| |
|
| | useEffect(() => { |
| | const container = containerRef.current; |
| | if (container) { |
| | const handleScroll = () => { |
| | const atBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 10; |
| | setUserScrolled(!atBottom); |
| | }; |
| | container.addEventListener('scroll', handleScroll); |
| | return () => container.removeEventListener('scroll', handleScroll); |
| | } |
| | }, []); |
| |
|
| | |
| | const handleCopy = () => { |
| | navigator.clipboard.writeText(value || '') |
| | .then(() => alert('Markdown 已复制到剪贴板')) |
| | .catch(err => console.error('复制失败:', err)); |
| | }; |
| |
|
| | |
| | const handleDownload = () => { |
| | const blob = new Blob([value || ''], { type: 'text/markdown;charset=utf-8' }); |
| | const url = URL.createObjectURL(blob); |
| | const a = document.createElement('a'); |
| | a.href = url; |
| | a.download = 'document.md'; |
| | document.body.appendChild(a); |
| | a.click(); |
| | document.body.removeChild(a); |
| | URL.revokeObjectURL(url); |
| | }; |
| |
|
| | return ( |
| | <div className="markdown-editor"> |
| | {/* <div className="markdown-toolbar"> |
| | <button className="neon-button" onClick={handleCopy}>复制 Markdown</button> |
| | <button className="neon-button" onClick={handleDownload}>下载 Markdown</button> |
| | </div> */} |
| | <div |
| | ref={containerRef} |
| | className="markdown-preview" |
| | dangerouslySetInnerHTML={{ __html: sanitizedHtml }} |
| | /> |
| | </div> |
| | ); |
| | } |
| | function SendRequestToBackend() { |
| | const [inputValue, setInputValue] = useState(''); |
| |
|
| | const handleSendRequest = async () => { |
| | try { |
| | const response = await fetch('http://localhost:8001/generate_survey', { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/json', |
| | }, |
| | body: JSON.stringify({ query: inputValue }), |
| | }); |
| |
|
| | if (!response.ok) { |
| | throw new Error('Failed to send request'); |
| | } |
| |
|
| | const data = await response.json(); |
| | console.log('Response from backend:', data); |
| | } catch (error) { |
| | console.error('Error sending request:', error); |
| | } |
| | }; |
| |
|
| | return ( |
| | <div className="request-panel" style={{ flexDirection: 'column', alignItems: 'center' }}> |
| | <input |
| | type="text" |
| | value={inputValue} |
| | onChange={(e) => setInputValue(e.target.value)} |
| | className="neon-input" |
| | placeholder="Enter text to send" |
| | rows={3} |
| | /> |
| | <button onClick={handleSendRequest} className="neon-button"> |
| | Go! |
| | </button> |
| | </div> |
| | ); |
| | } |
| |
|
| |
|
| | function App() { |
| | const [inputs, setInputs] = useState({ |
| | query: { title: 'Query', displayText: '', targetText: '', isTyping: false }, |
| | nowUpdate: { title: 'Now Update', displayText: '', targetText: '', isTyping: false }, |
| | nextUpdate: { title: 'Next Update', displayText: '', targetText: '', isTyping: false }, |
| | searchKeywords: { title: 'Search Keywords', displayText: '', targetText: '', isTyping: false }, |
| | papers: { title: 'Papers', displayText: '', targetText: '', isTyping: false }, |
| | }); |
| |
|
| | const [markdownContent, setMarkdownContent] = useState(''); |
| |
|
| | const inputKeyMap = { |
| | query: inputs.query, |
| | nowUpdate: inputs.nowUpdate, |
| | nextUpdate: inputs.nextUpdate, |
| | searchKeywords: inputs.searchKeywords, |
| | papers: inputs.papers, |
| | markdown: markdownContent |
| | }; |
| |
|
| | const updateInputsFromPostData = (postData) => { |
| | let newMarkdownContent = markdownContent; |
| | |
| | Object.entries(postData).forEach(([key, value]) => { |
| | if (key in inputKeyMap) { |
| | if (key === 'markdown') { |
| | if (markdownContent !== value) { |
| | newMarkdownContent = value; |
| | setMarkdownContent(newMarkdownContent); |
| | } |
| | } else if (inputKeyMap[key] && inputKeyMap[key].targetText !== value) { |
| | const updatedInput = { |
| | ...inputKeyMap[key], |
| | targetText: value, |
| | isTyping: true, |
| | }; |
| | setInputs((prevInputs) => ({ |
| | ...prevInputs, |
| | [key]: updatedInput, |
| | })); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | } |
| | } |
| | }); |
| |
|
| | |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | useEffect(() => { |
| | const ws = new WebSocket('ws://localhost:8001/ws'); |
| | ws.onmessage = (event) => { |
| | try { |
| | const data = JSON.parse(event.data); |
| | updateInputsFromPostData(data); |
| | console.log('Received data:', data); |
| | } catch (e) { |
| | console.error('Invalid WebSocket message:', e); |
| | } |
| | }; |
| | ws.onerror = (err) => { |
| | console.error('WebSocket error:', err); |
| | }; |
| | |
| | }, []); |
| |
|
| | const leftInputs = [inputs.nowUpdate, inputs.nextUpdate,inputs.searchKeywords]; |
| | const rightInputs = [inputs.papers]; |
| |
|
| | return ( |
| | <div className="cyber-container"> |
| | <div className="tech-panel left-panel"> |
| | {leftInputs.map((input, index) => ( |
| | <div key={`left-${index}`} className="input-wrapper"> |
| | <h3 className="input-title" style={{ fontSize: '14px' }}>{input.title}</h3> |
| | <textarea |
| | value={input.targetText} |
| | readOnly |
| | className="neon-input" |
| | rows={Math.max(10, input.targetText.split('\n').length)} |
| | cols={50} |
| | style={{ resize: 'none', fontSize: '12px' }} |
| | /> |
| | </div> |
| | ))} |
| | </div> |
| | |
| | <div className="core-module"> |
| | <SendRequestToBackend /> |
| | <MarkdownEditor value={markdownContent} /> |
| | </div> |
| | |
| | <div className="tech-panel right-panel"> |
| | {rightInputs.map((input, index) => ( |
| | <div key={`right-${index}`} className="input-wrapper"> |
| | <h3 className="input-title" style={{ fontSize: '14px' }}>{input.title}</h3> |
| | <textarea |
| | value={input.targetText} |
| | readOnly |
| | className="neon-input" |
| | rows={100} |
| | cols={50} |
| | style={{ resize: 'none', fontSize: '12px' }} |
| | /> |
| | </div> |
| | ))} |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|
| | export default App; |