429 lines
12 KiB
HTML
429 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🐉 数学小卧龙 — 苏格拉底导师</title>
|
||
<style>
|
||
:root {
|
||
--bg: #FFF8F0;
|
||
--card: #FFFFFF;
|
||
--primary: #FF6B6B;
|
||
--secondary: #4ECDC4;
|
||
--accent: #45B7D1;
|
||
--text: #2D3436;
|
||
--light: #F8F9FA;
|
||
--border: #E9ECEF;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 顶部栏 */
|
||
header {
|
||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||
color: white;
|
||
padding: 12px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
header .logo { font-size: 28px; }
|
||
header h1 { font-size: 18px; font-weight: 600; }
|
||
header .tag { font-size: 12px; opacity: 0.85; }
|
||
|
||
/* 三栏布局 */
|
||
main {
|
||
display: grid;
|
||
grid-template-columns: 1fr 380px;
|
||
grid-template-rows: 1fr 1fr;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
height: calc(100vh - 56px);
|
||
}
|
||
|
||
/* 左侧主区域 */
|
||
.main-area {
|
||
grid-row: 1 / 3;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 画布区 */
|
||
.canvas-panel {
|
||
flex: 1;
|
||
background: var(--card);
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.canvas-toolbar {
|
||
display: flex;
|
||
gap: 6px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
flex-wrap: wrap;
|
||
}
|
||
.canvas-toolbar button {
|
||
padding: 5px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--light);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
}
|
||
.canvas-toolbar button:hover { background: var(--secondary); color: white; border-color: var(--secondary); }
|
||
.canvas-toolbar button.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||
.canvas-toolbar .color-pick { width: 28px; height: 28px; border-radius: 50%; border: 2px solid var(--border); cursor: pointer; padding: 0; }
|
||
|
||
.drawing-area {
|
||
flex: 1;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
canvas { position: absolute; top: 0; left: 0; cursor: crosshair; }
|
||
.svg-display {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
pointer-events: none; opacity: 0.3;
|
||
}
|
||
.svg-display svg { max-width: 90%; max-height: 90%; }
|
||
|
||
/* 输入区 */
|
||
.input-panel {
|
||
background: var(--card);
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||
padding: 12px;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.input-panel textarea {
|
||
flex: 1;
|
||
border: 2px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 15px;
|
||
resize: none;
|
||
height: 60px;
|
||
font-family: inherit;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.input-panel textarea:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
}
|
||
.input-panel button {
|
||
padding: 10px 24px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
}
|
||
.input-panel button:hover { transform: scale(1.03); }
|
||
.input-panel button:active { transform: scale(0.97); }
|
||
.input-panel .draw-btn { background: var(--secondary); }
|
||
|
||
/* 右侧面板 */
|
||
.right-panels {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.panel {
|
||
background: var(--card);
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||
padding: 14px;
|
||
overflow-y: auto;
|
||
}
|
||
.panel h3 {
|
||
font-size: 14px;
|
||
color: var(--accent);
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
/* 对话区 */
|
||
.chat-panel {
|
||
flex: 2;
|
||
}
|
||
.chat-messages {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.chat-msg {
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
animation: fadeIn 0.3s;
|
||
}
|
||
.chat-msg.student {
|
||
background: var(--light);
|
||
align-self: flex-end;
|
||
max-width: 85%;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
.chat-msg.tutor {
|
||
background: linear-gradient(135deg, #E8F8F5, #D5F5E3);
|
||
align-self: flex-start;
|
||
max-width: 85%;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
.chat-msg .role { font-size: 11px; color: #999; margin-bottom: 4px; }
|
||
.chat-msg .hint { font-size: 12px; color: var(--accent); margin-top: 6px; padding-top: 6px; border-top: 1px dashed #ccc; }
|
||
.typing { opacity: 0.6; }
|
||
.typing::after { content: "…"; animation: dots 1.5s infinite; }
|
||
@keyframes dots { 0%,20% { content: "." } 40% { content: ".." } 60%,100% { content: "…" } }
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||
|
||
/* SVG 展示区 */
|
||
.svg-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 180px;
|
||
}
|
||
.svg-panel .svg-container {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.svg-panel .svg-container svg {
|
||
max-width: 100%;
|
||
max-height: 200px;
|
||
}
|
||
.svg-placeholder {
|
||
color: #ccc;
|
||
font-size: 14px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 快捷按钮 */
|
||
.quick-btns {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
.quick-btns button {
|
||
padding: 4px 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
background: var(--light);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
}
|
||
.quick-btns button:hover { background: var(--accent); color: white; border-color: var(--accent); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<span class="logo">🐉</span>
|
||
<div>
|
||
<h1>数学小卧龙</h1>
|
||
<span class="tag">苏格拉底式导师 · 不直接给答案</span>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<!-- 左侧:画布 + 输入 -->
|
||
<div class="main-area">
|
||
<div class="canvas-panel" id="canvasPanel">
|
||
<div class="canvas-toolbar">
|
||
<button onclick="setTool('pen')" class="active" id="penBtn">✏️ 画笔</button>
|
||
<button onclick="setTool('eraser')" id="eraserBtn">🧹 橡皮</button>
|
||
<input type="color" value="#FF6B6B" class="color-pick" id="colorPicker" onchange="setColor(this.value)">
|
||
<button onclick="clearCanvas()">🗑️ 清空</button>
|
||
<span style="flex:1"></span>
|
||
<button onclick="drawShape('pie',[1,1,1,1])">🥧 画饼图</button>
|
||
<button onclick="drawShape('numberline',{start:0,end:10})">📏 数轴</button>
|
||
<button onclick="drawShape('grid',{rows:3,cols:4})">🔲 格子阵</button>
|
||
</div>
|
||
<div class="drawing-area" id="drawingArea">
|
||
<canvas id="drawCanvas"></canvas>
|
||
<div class="svg-display" id="svgOverlay"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-panel">
|
||
<textarea id="questionInput" placeholder="问我任何数学问题吧!比如:½ + ¼ 等于多少?或者 23×17 怎么算?" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();askQuestion()}"></textarea>
|
||
<button onclick="askQuestion()">🚀 提问</button>
|
||
<button class="draw-btn" onclick="drawRequest()">🎨 画图</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:对话 + SVG -->
|
||
<div class="right-panels">
|
||
<div class="panel chat-panel">
|
||
<h3>💬 苏格拉底对话</h3>
|
||
<div class="quick-btns">
|
||
<button onclick="askPreset('½ + ¼ 等于多少?')">分数加法</button>
|
||
<button onclick="askPreset('23×17 怎么算?')">乘法</button>
|
||
<button onclick="askPreset('三角形的面积怎么求?')">几何面积</button>
|
||
<button onclick="askPreset('什么是周长?')">周长</button>
|
||
</div>
|
||
<div class="chat-messages" id="chatMessages">
|
||
<div class="chat-msg tutor">
|
||
<div class="role">🐉 小卧龙</div>
|
||
你好!我是数学小卧龙 🐉<br>我不会直接告诉你答案——但我会用提问和画画,帮你自己发现!准备好了吗?
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel svg-panel">
|
||
<h3>📐 可视化</h3>
|
||
<div class="svg-container" id="svgContainer">
|
||
<div class="svg-placeholder">提问后这里会出现图形哦~</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
// === 画布引擎 ===
|
||
const canvas = document.getElementById('drawCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
let tool = 'pen';
|
||
let color = '#FF6B6B';
|
||
let drawing = false;
|
||
|
||
function resizeCanvas() {
|
||
const area = document.getElementById('drawingArea');
|
||
canvas.width = area.clientWidth;
|
||
canvas.height = area.clientHeight;
|
||
}
|
||
resizeCanvas();
|
||
window.addEventListener('resize', resizeCanvas);
|
||
|
||
canvas.addEventListener('mousedown', e => { drawing = true; draw(e); });
|
||
canvas.addEventListener('mousemove', e => { if(drawing) draw(e); });
|
||
canvas.addEventListener('mouseup', () => drawing = false);
|
||
canvas.addEventListener('mouseleave', () => drawing = false);
|
||
canvas.addEventListener('touchstart', e => { drawing = true; draw(e.touches[0]); });
|
||
canvas.addEventListener('touchmove', e => { if(drawing) draw(e.touches[0]); e.preventDefault(); });
|
||
canvas.addEventListener('touchend', () => drawing = false);
|
||
|
||
function draw(e) {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const y = e.clientY - rect.top;
|
||
ctx.lineWidth = tool === 'eraser' ? 20 : 3;
|
||
ctx.strokeStyle = tool === 'eraser' ? '#FFF8F0' : color;
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
if (tool === 'eraser') {
|
||
ctx.clearRect(x-10, y-10, 20, 20);
|
||
} else {
|
||
ctx.lineTo(x, y);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, y);
|
||
}
|
||
}
|
||
|
||
function setTool(t) {
|
||
tool = t;
|
||
document.getElementById('penBtn').classList.toggle('active', t==='pen');
|
||
document.getElementById('eraserBtn').classList.toggle('active', t==='eraser');
|
||
}
|
||
function setColor(c) { color = c; }
|
||
function clearCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); }
|
||
|
||
// === 对话引擎 ===
|
||
async function askQuestion() {
|
||
const input = document.getElementById('questionInput');
|
||
const q = input.value.trim();
|
||
if (!q) return;
|
||
input.value = '';
|
||
|
||
addMessage('student', q);
|
||
addMessage('tutor', '', true); // typing indicator
|
||
|
||
try {
|
||
const resp = await fetch('/ask', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({question: q})
|
||
});
|
||
const data = await resp.json();
|
||
|
||
// 移除 typing
|
||
const msgs = document.getElementById('chatMessages');
|
||
msgs.removeChild(msgs.lastChild);
|
||
|
||
addMessage('tutor', data.reply, false, data.visual_hint);
|
||
if (data.svg) {
|
||
document.getElementById('svgContainer').innerHTML = data.svg;
|
||
}
|
||
} catch(e) {
|
||
const msgs = document.getElementById('chatMessages');
|
||
msgs.removeChild(msgs.lastChild);
|
||
addMessage('tutor', '哎呀,网络好像出了点问题... 再试一次?😅');
|
||
}
|
||
}
|
||
|
||
function askPreset(q) {
|
||
document.getElementById('questionInput').value = q;
|
||
askQuestion();
|
||
}
|
||
|
||
function addMessage(role, text, typing=false, hint='') {
|
||
const div = document.createElement('div');
|
||
div.className = `chat-msg ${role}${typing ? ' typing' : ''}`;
|
||
div.innerHTML = `<div class="role">${role==='student' ? '🧒 你' : '🐉 小卧龙'}</div>${text}${hint ? `<div class="hint">💡 ${hint}</div>` : ''}`;
|
||
const msgs = document.getElementById('chatMessages');
|
||
msgs.appendChild(div);
|
||
msgs.scrollTop = msgs.scrollHeight;
|
||
}
|
||
|
||
// === 画图请求 ===
|
||
async function drawRequest() {
|
||
const q = document.getElementById('questionInput').value.trim();
|
||
if (!q) return;
|
||
try {
|
||
const resp = await fetch('/draw', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({type: 'pie', params: {fractions: [1,1,2,1], title: '你的问题可视化'}})
|
||
});
|
||
const data = await resp.json();
|
||
if (data.svg) {
|
||
document.getElementById('svgContainer').innerHTML = data.svg;
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function drawShape(type, params) {
|
||
fetch('/draw', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({type, params})
|
||
}).then(r=>r.json()).then(d=>{
|
||
if(d.svg) document.getElementById('svgContainer').innerHTML = d.svg;
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|