feat(lesson-preparation): 师生交互编辑器(对话轮次增删改)
This commit is contained in:
@@ -0,0 +1,250 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import type { InteractionBlockData, QATurn } from "../../types";
|
||||||
|
import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeId: string;
|
||||||
|
data: InteractionBlockData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QaEditor({ nodeId, data }: Props) {
|
||||||
|
const t = useTranslations("lessonPreparation");
|
||||||
|
const updateNode = useLessonPlanEditor((s) => s.updateNode);
|
||||||
|
|
||||||
|
const updateData = (next: InteractionBlockData) => {
|
||||||
|
updateNode(nodeId, { data: next });
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTurn = () => {
|
||||||
|
const newTurn: QATurn = {
|
||||||
|
id: createId(),
|
||||||
|
role: "teacher",
|
||||||
|
content: "",
|
||||||
|
expectedAnswer: "",
|
||||||
|
order: data.turns.length,
|
||||||
|
};
|
||||||
|
updateData({ ...data, turns: [...data.turns, newTurn] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTurn = (id: string, patch: Partial<QATurn>) => {
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
turns: data.turns.map((t) => (t.id === id ? { ...t, ...patch } : t)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTurn = (id: string) => {
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
turns: data.turns
|
||||||
|
.filter((t) => t.id !== id)
|
||||||
|
.map((t, i) => ({ ...t, order: i })),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveTurn = (id: string, direction: "up" | "down") => {
|
||||||
|
const idx = data.turns.findIndex((t) => t.id === id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
const newIdx = direction === "up" ? idx - 1 : idx + 1;
|
||||||
|
if (newIdx < 0 || newIdx >= data.turns.length) return;
|
||||||
|
const turns = [...data.turns];
|
||||||
|
[turns[idx], turns[newIdx]] = [turns[newIdx], turns[idx]];
|
||||||
|
updateData({
|
||||||
|
...data,
|
||||||
|
turns: turns.map((t, i) => ({ ...t, order: i })),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
display: "block",
|
||||||
|
marginBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("v4.detail.designIntent")}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={data.designIntent}
|
||||||
|
onChange={(e) => updateData({ ...data, designIntent: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: 5,
|
||||||
|
padding: "8px 10px",
|
||||||
|
background: "var(--background)",
|
||||||
|
fontFamily: "Inter, sans-serif",
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: "var(--foreground)",
|
||||||
|
resize: "vertical",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
display: "block",
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("v4.detail.qaDialog")}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{data.turns.map((turn, idx) => (
|
||||||
|
<QaTurnEditor
|
||||||
|
key={turn.id}
|
||||||
|
turn={turn}
|
||||||
|
index={idx}
|
||||||
|
total={data.turns.length}
|
||||||
|
onUpdate={(patch) => updateTurn(turn.id, patch)}
|
||||||
|
onRemove={() => removeTurn(turn.id)}
|
||||||
|
onMoveUp={() => moveTurn(turn.id, "up")}
|
||||||
|
onMoveDown={() => moveTurn(turn.id, "down")}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addTurn}
|
||||||
|
style={{
|
||||||
|
border: "1px dashed var(--border)",
|
||||||
|
background: "transparent",
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 5,
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ {t("v4.detail.addTurn")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TurnProps {
|
||||||
|
turn: QATurn;
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
onUpdate: (patch: Partial<QATurn>) => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
onMoveUp: () => void;
|
||||||
|
onMoveDown: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMoveDown }: TurnProps) {
|
||||||
|
const t = useTranslations("lessonPreparation");
|
||||||
|
const isTeacher = turn.role === "teacher";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
borderRadius: 5,
|
||||||
|
padding: "8px 10px",
|
||||||
|
background: "var(--background)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 6,
|
||||||
|
fontSize: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
value={turn.role}
|
||||||
|
onChange={(e) => onUpdate({ role: e.target.value as "teacher" | "student" })}
|
||||||
|
className={isTeacher ? "teacher" : "student"}
|
||||||
|
style={{
|
||||||
|
fontFamily: "JetBrains Mono, monospace",
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: "2px 6px",
|
||||||
|
borderRadius: 2,
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
background: "var(--background)",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: isTeacher ? "#1c1917" : "#6b7280",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="teacher">{t("v4.detail.turnTeacher")}</option>
|
||||||
|
<option value="student">{t("v4.detail.turnStudent")}</option>
|
||||||
|
</select>
|
||||||
|
<span style={{ color: "var(--muted-foreground)", fontSize: 10 }}>
|
||||||
|
{t("v4.interaction.round", { n: index + 1 })}
|
||||||
|
</span>
|
||||||
|
<div style={{ marginLeft: "auto" }}>
|
||||||
|
<button type="button" onClick={onMoveUp} disabled={index === 0} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: index === 0 ? "default" : "pointer", padding: "2px 4px", fontSize: 11 }}>↑</button>
|
||||||
|
<button type="button" onClick={onMoveDown} disabled={index === total - 1} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: index === total - 1 ? "default" : "pointer", padding: "2px 4px", fontSize: 11 }}>↓</button>
|
||||||
|
<button type="button" onClick={onRemove} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: "pointer", padding: "2px 4px", fontSize: 11 }}>×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={turn.content}
|
||||||
|
onChange={(e) => onUpdate({ content: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
placeholder="内容"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "transparent",
|
||||||
|
resize: "vertical",
|
||||||
|
fontFamily: "Inter, sans-serif",
|
||||||
|
fontSize: 12.5,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: "var(--foreground)",
|
||||||
|
minHeight: 36,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isTeacher && (
|
||||||
|
<textarea
|
||||||
|
value={turn.expectedAnswer ?? ""}
|
||||||
|
onChange={(e) => onUpdate({ expectedAnswer: e.target.value })}
|
||||||
|
rows={1}
|
||||||
|
placeholder={t("v4.detail.expectedAnswer")}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "var(--muted)",
|
||||||
|
resize: "vertical",
|
||||||
|
fontFamily: "Inter, sans-serif",
|
||||||
|
fontSize: 11,
|
||||||
|
fontStyle: "italic",
|
||||||
|
color: "var(--muted-foreground)",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderRadius: 3,
|
||||||
|
marginTop: 4,
|
||||||
|
minHeight: 24,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user