初始化项目,由ModelHub XC社区提供模型
Model: OpenBMB/MiniCPM4-Survey Source: Original Platform
This commit is contained in:
24
code/frontend/minicpm4-survey/.gitignore
vendored
Normal file
24
code/frontend/minicpm4-survey/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
33
code/frontend/minicpm4-survey/eslint.config.js
Normal file
33
code/frontend/minicpm4-survey/eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
13
code/frontend/minicpm4-survey/index.html
Normal file
13
code/frontend/minicpm4-survey/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/openbmb.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MiniCPM4-Survey</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2822
code/frontend/minicpm4-survey/package-lock.json
generated
Normal file
2822
code/frontend/minicpm4-survey/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
code/frontend/minicpm4-survey/package.json
Normal file
29
code/frontend/minicpm4-survey/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "minicpm4-survey",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.2.6",
|
||||
"marked": "^15.0.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
BIN
code/frontend/minicpm4-survey/public/openbmb.png
Normal file
BIN
code/frontend/minicpm4-survey/public/openbmb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 B |
318
code/frontend/minicpm4-survey/src/App.css
Normal file
318
code/frontend/minicpm4-survey/src/App.css
Normal file
@@ -0,0 +1,318 @@
|
||||
:root {
|
||||
--neon-blue: #00f3ff;
|
||||
--neon-purple: #ffffff;
|
||||
--dark-panel: rgba(10, 10, 30, 0.95);
|
||||
--glass-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Orbitron', monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cyber-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(circle at center, #0a0a1f 0%, #000000 100%);
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 全息背景 */
|
||||
.hologram-bg {
|
||||
position: fixed;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 5px,
|
||||
rgba(0, 255, 255, 0.05) 5px,
|
||||
rgba(0, 255, 255, 0.05) 10px
|
||||
);
|
||||
animation: scan 20s linear infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { transform: translateY(-50%) rotate(0deg); }
|
||||
100% { transform: translateY(50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 面板样式 */
|
||||
.tech-panel {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
padding: 1.5rem;
|
||||
background: var(--glass-color);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 255, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tech-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple), var(--neon-blue));
|
||||
animation: pulse 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.neon-input {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(0, 255, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.neon-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.neon-input:focus {
|
||||
border-color: var(--neon-blue);
|
||||
box-shadow: 0 0 10px var(--neon-blue);
|
||||
}
|
||||
|
||||
.input-glow {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--neon-purple), transparent);
|
||||
animation: glowPulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes glowPulse {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 核心区域 */
|
||||
.core-module {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.quantum-textarea {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
font-size: 1.2rem;
|
||||
background: rgba(0, 0, 20, 0.7);
|
||||
border: 2px solid var(--neon-blue);
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
resize: both;
|
||||
min-height: 300px;
|
||||
backdrop-filter: blur(5px);
|
||||
font-family: 'Orbitron', monospace;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quantum-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.quantum-textarea:focus {
|
||||
border-color: var(--neon-purple);
|
||||
box-shadow: 0 0 20px var(--neon-purple);
|
||||
}
|
||||
|
||||
.core-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
background: radial-gradient(circle, var(--neon-blue) 0%, transparent 70%);
|
||||
opacity: 0.1;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
animation: pulseGlow 5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0% { transform: translate(-50%, -50%) scale(1); }
|
||||
100% { transform: translate(-50%, -50%) scale(1.2); }
|
||||
}
|
||||
|
||||
/* 数据流动画 */
|
||||
.data-stream {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 2rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stream-pulse {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--neon-blue);
|
||||
border-radius: 50%;
|
||||
animation: pulseDot 1.5s infinite;
|
||||
}
|
||||
|
||||
.delay-1 {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.delay-2 {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes pulseDot {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
70% { transform: scale(1.5); opacity: 0.3; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1024px) {
|
||||
.cyber-container {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tech-panel {
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.core-module {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
.markdown-editor {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.markdown-input,
|
||||
.markdown-preview {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
border: 2px solid var(--neon-blue);
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 20, 0.7);
|
||||
color: #fff;
|
||||
font-family: 'Orbitron', monospace;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.markdown-input {
|
||||
min-height: 300px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.markdown-input:focus {
|
||||
box-shadow: 0 0 10px var(--neon-blue);
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
max-height: 800px; /* 设置最大高度,超出后滚动 */
|
||||
width: 100%;
|
||||
overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
|
||||
padding: 10px;
|
||||
border: 1px solid #333;
|
||||
background-color: #111;
|
||||
color: #eee;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Markdown 内容增强样式 */
|
||||
.markdown-preview h1, h2, h3 {
|
||||
color: var(--neon-purple);
|
||||
}
|
||||
|
||||
.markdown-preview pre {
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
color: #00ffcc;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-preview code {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--neon-blue);
|
||||
}
|
||||
|
||||
.core-module {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.markdown-toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.neon-button {
|
||||
background-color: #000;
|
||||
color: #0f0;
|
||||
border: 2px solid #0f0;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.neon-button:hover {
|
||||
background-color: #0f0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.markdown-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 1rem;
|
||||
}
|
||||
259
code/frontend/minicpm4-survey/src/App.jsx
Normal file
259
code/frontend/minicpm4-survey/src/App.jsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import './App.css';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
|
||||
// 自定义 hook:防抖
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 复制 Markdown 内容
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(value || '')
|
||||
.then(() => alert('Markdown 已复制到剪贴板'))
|
||||
.catch(err => console.error('复制失败:', err));
|
||||
};
|
||||
|
||||
// 下载 Markdown 文件
|
||||
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,
|
||||
}));
|
||||
|
||||
// startTypingAnimationForTextbox(value, (newText) => {
|
||||
// setInputs((prevInputs) => ({
|
||||
// ...prevInputs,
|
||||
// [key]: {
|
||||
// ...prevInputs[key],
|
||||
// displayText: newText,
|
||||
// },
|
||||
// }));
|
||||
// });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
// const startTypingAnimationForTextbox = (text, setText) => {
|
||||
// setText('');
|
||||
// let charIndex = 0;
|
||||
// const timer = setInterval(() => {
|
||||
// if (charIndex < text.length) {
|
||||
// setText((prev) => prev + text[charIndex]);
|
||||
// charIndex++;
|
||||
// } else {
|
||||
// clearInterval(timer);
|
||||
// }
|
||||
// }, 50); // Reduced interval for faster typing animation
|
||||
// };
|
||||
|
||||
|
||||
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);
|
||||
};
|
||||
// return () => ws.close();
|
||||
}, []);
|
||||
|
||||
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;
|
||||
68
code/frontend/minicpm4-survey/src/index.css
Normal file
68
code/frontend/minicpm4-survey/src/index.css
Normal file
@@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
10
code/frontend/minicpm4-survey/src/main.jsx
Normal file
10
code/frontend/minicpm4-survey/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
7
code/frontend/minicpm4-survey/vite.config.js
Normal file
7
code/frontend/minicpm4-survey/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user