| 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; |