fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
295
desktop/src/components/classroom_player/WhiteboardCanvas.tsx
Normal file
295
desktop/src/components/classroom_player/WhiteboardCanvas.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* WhiteboardCanvas — SVG-based whiteboard for classroom scene rendering.
|
||||
*
|
||||
* Supports incremental drawing operations:
|
||||
* - Text (positioned labels)
|
||||
* - Shapes (rectangles, circles, arrows)
|
||||
* - Charts (bar/line/pie via simple SVG)
|
||||
* - LaTeX (rendered as styled text blocks)
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import type { SceneAction } from '../../types/classroom';
|
||||
|
||||
interface WhiteboardCanvasProps {
|
||||
items: WhiteboardItem[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface WhiteboardItem {
|
||||
type: string;
|
||||
data: SceneAction;
|
||||
}
|
||||
|
||||
export function WhiteboardCanvas({
|
||||
items,
|
||||
width = 800,
|
||||
height = 600,
|
||||
}: WhiteboardCanvasProps) {
|
||||
const renderItem = useCallback((item: WhiteboardItem, index: number) => {
|
||||
switch (item.type) {
|
||||
case 'whiteboard_draw_text':
|
||||
return <TextItem key={index} data={item.data as TextDrawData} />;
|
||||
case 'whiteboard_draw_shape':
|
||||
return <ShapeItem key={index} data={item.data as ShapeDrawData} />;
|
||||
case 'whiteboard_draw_chart':
|
||||
return <ChartItem key={index} data={item.data as ChartDrawData} />;
|
||||
case 'whiteboard_draw_latex':
|
||||
return <LatexItem key={index} data={item.data as LatexDrawData} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 overflow-auto">
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className="w-full h-full"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Grid background */}
|
||||
<defs>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#f0f0f0" strokeWidth="0.5" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width={width} height={height} fill="url(#grid)" />
|
||||
|
||||
{/* Rendered items */}
|
||||
{items.map((item, i) => renderItem(item, i))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface TextDrawData {
|
||||
type: 'whiteboard_draw_text';
|
||||
x: number;
|
||||
y: number;
|
||||
text: string;
|
||||
fontSize?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function TextItem({ data }: { data: TextDrawData }) {
|
||||
return (
|
||||
<text
|
||||
x={data.x}
|
||||
y={data.y}
|
||||
fontSize={data.fontSize ?? 16}
|
||||
fill={data.color ?? '#333333'}
|
||||
fontFamily="system-ui, sans-serif"
|
||||
>
|
||||
{data.text}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
interface ShapeDrawData {
|
||||
type: 'whiteboard_draw_shape';
|
||||
shape: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
function ShapeItem({ data }: { data: ShapeDrawData }) {
|
||||
switch (data.shape) {
|
||||
case 'circle':
|
||||
return (
|
||||
<ellipse
|
||||
cx={data.x + data.width / 2}
|
||||
cy={data.y + data.height / 2}
|
||||
rx={data.width / 2}
|
||||
ry={data.height / 2}
|
||||
fill={data.fill ?? '#e5e7eb'}
|
||||
stroke="#9ca3af"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
);
|
||||
case 'arrow':
|
||||
return (
|
||||
<g>
|
||||
<line
|
||||
x1={data.x}
|
||||
y1={data.y + data.height / 2}
|
||||
x2={data.x + data.width}
|
||||
y2={data.y + data.height / 2}
|
||||
stroke={data.fill ?? '#6b7280'}
|
||||
strokeWidth={2}
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill={data.fill ?? '#6b7280'} />
|
||||
</marker>
|
||||
</defs>
|
||||
</g>
|
||||
);
|
||||
default: // rectangle
|
||||
return (
|
||||
<rect
|
||||
x={data.x}
|
||||
y={data.y}
|
||||
width={data.width}
|
||||
height={data.height}
|
||||
fill={data.fill ?? '#e5e7eb'}
|
||||
stroke="#9ca3af"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ChartDrawData {
|
||||
type: 'whiteboard_draw_chart';
|
||||
chartType: string;
|
||||
data: Record<string, unknown>;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function ChartItem({ data }: { data: ChartDrawData }) {
|
||||
const chartData = data.data;
|
||||
const labels = (chartData?.labels as string[]) ?? [];
|
||||
const values = (chartData?.values as number[]) ?? [];
|
||||
|
||||
if (labels.length === 0 || values.length === 0) return null;
|
||||
|
||||
switch (data.chartType) {
|
||||
case 'bar':
|
||||
return <BarChart data={data} labels={labels} values={values} />;
|
||||
case 'line':
|
||||
return <LineChart data={data} labels={labels} values={values} />;
|
||||
default:
|
||||
return <BarChart data={data} labels={labels} values={values} />;
|
||||
}
|
||||
}
|
||||
|
||||
function BarChart({ data, labels, values }: {
|
||||
data: ChartDrawData;
|
||||
labels: string[];
|
||||
values: number[];
|
||||
}) {
|
||||
const maxVal = Math.max(...values, 1);
|
||||
const barWidth = data.width / (labels.length * 2);
|
||||
const chartHeight = data.height - 30;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${data.x}, ${data.y})`}>
|
||||
{values.map((val, i) => {
|
||||
const barHeight = (val / maxVal) * chartHeight;
|
||||
return (
|
||||
<g key={i}>
|
||||
<rect
|
||||
x={i * (barWidth * 2) + barWidth / 2}
|
||||
y={chartHeight - barHeight}
|
||||
width={barWidth}
|
||||
height={barHeight}
|
||||
fill="#6366f1"
|
||||
rx={2}
|
||||
/>
|
||||
<text
|
||||
x={i * (barWidth * 2) + barWidth}
|
||||
y={data.height - 5}
|
||||
textAnchor="middle"
|
||||
fontSize={10}
|
||||
fill="#666"
|
||||
>
|
||||
{labels[i]}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function LineChart({ data, labels, values }: {
|
||||
data: ChartDrawData;
|
||||
labels: string[];
|
||||
values: number[];
|
||||
}) {
|
||||
const maxVal = Math.max(...values, 1);
|
||||
const chartHeight = data.height - 30;
|
||||
const stepX = data.width / Math.max(labels.length - 1, 1);
|
||||
|
||||
const points = values.map((val, i) => {
|
||||
const x = i * stepX;
|
||||
const y = chartHeight - (val / maxVal) * chartHeight;
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<g transform={`translate(${data.x}, ${data.y})`}>
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{values.map((val, i) => {
|
||||
const x = i * stepX;
|
||||
const y = chartHeight - (val / maxVal) * chartHeight;
|
||||
return (
|
||||
<g key={i}>
|
||||
<circle cx={x} cy={y} r={3} fill="#6366f1" />
|
||||
<text
|
||||
x={x}
|
||||
y={data.height - 5}
|
||||
textAnchor="middle"
|
||||
fontSize={10}
|
||||
fill="#666"
|
||||
>
|
||||
{labels[i]}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
interface LatexDrawData {
|
||||
type: 'whiteboard_draw_latex';
|
||||
latex: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function LatexItem({ data }: { data: LatexDrawData }) {
|
||||
return (
|
||||
<g transform={`translate(${data.x}, ${data.y})`}>
|
||||
<rect
|
||||
x={-4}
|
||||
y={-20}
|
||||
width={data.latex.length * 10 + 8}
|
||||
height={28}
|
||||
fill="#fef3c7"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
/>
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
fontSize={14}
|
||||
fill="#92400e"
|
||||
fontFamily="'Courier New', monospace"
|
||||
>
|
||||
{data.latex}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user