feat: Enhance AutoScore functionality with interval triggers and JSON tree support

- Added support for trigger trees in AutoScoreRule interface.
- Implemented functions to convert query trees to JSON and vice versa.
- Updated IntervalValue to use days, hours, and minutes instead of a single amount and unit.
- Refactored IntervalValueWidget to allow separate input for days, hours, and minutes.
- Added validation to ensure at least one interval trigger is present.
- Enhanced user interface with informative alerts and tooltips for better user experience.
- Updated localization files to include new strings for interval triggers and UI elements.
This commit is contained in:
NanGua-QWQ
2026-04-10 21:09:04 +08:00
parent 3caea0afe5
commit eef6b703f7
13 changed files with 931 additions and 294 deletions
@@ -51,6 +51,7 @@ export interface AutoScoreRule {
enabled: boolean
studentNames: string[]
triggers: AutoScoreTrigger[]
triggerTree?: JsonGroup | null
actions: AutoScoreAction[]
execution?: AutoScoreExecutionConfig
lastExecuted?: string | null
@@ -368,6 +369,70 @@ export const queryTreeToTriggers = (tree: ImmutableTree, config: Config): AutoSc
return collectTriggersFromItems(jsonTree.children1)
}
export const queryTreeToJson = (tree: ImmutableTree, config: Config): JsonGroup | null => {
const checkedTree = QbUtils.checkTree(tree, config)
const jsonTree = QbUtils.getTree(checkedTree, false, true)
if (!jsonTree || jsonTree.type !== "group") return null
return jsonTree as JsonGroup
}
const hydrateTriggerTreeNode = (
node: JsonItem,
fallbackTriggers: AutoScoreTrigger[],
fallbackIndex: { current: number }
): JsonItem | null => {
if (node.type === "group") {
const children = Array.isArray(node.children1)
? node.children1
.map((child) => hydrateTriggerTreeNode(child, fallbackTriggers, fallbackIndex))
.filter((child): child is JsonItem => Boolean(child))
: []
return {
...node,
children1: children,
}
}
if (node.type !== "rule") {
return node
}
const fallbackTrigger = fallbackTriggers[fallbackIndex.current]
fallbackIndex.current += 1
const parsed = triggerFromRule(node)
if (parsed) {
return node
}
if (!fallbackTrigger) {
return node
}
const fallbackRule = ruleFromTrigger(fallbackTrigger)
if (!fallbackRule) {
return node
}
return {
...node,
properties: fallbackRule.properties,
}
}
export const triggerTreeJsonToQueryTree = (
config: Config,
triggerTree?: JsonGroup | null,
fallbackTriggers: AutoScoreTrigger[] = []
): ImmutableTree => {
if (triggerTree && triggerTree.type === "group") {
const hydratedTree = hydrateTriggerTreeNode(triggerTree, fallbackTriggers, { current: 0 })
if (hydratedTree && hydratedTree.type === "group") {
return QbUtils.checkTree(QbUtils.loadTree(hydratedTree), config)
}
}
return triggersToQueryTree(config, fallbackTriggers)
}
const hasUnsupportedLogicInGroup = (group: JsonGroup): boolean => {
const conjunction =
typeof group.properties?.conjunction === "string" ? group.properties.conjunction : "AND"
+69 -27
View File
@@ -1,44 +1,87 @@
export type IntervalUnit = "minute" | "day" | "month"
export interface IntervalValue {
days: number
hours: number
minutes: number
}
interface LegacyIntervalValue {
amount: number
unit: IntervalUnit
}
export const DEFAULT_INTERVAL_UNIT: IntervalUnit = "day"
const normalizePositiveInteger = (value: unknown): number | null => {
const normalizeNonNegativeInteger = (value: unknown): number | null => {
if (typeof value === "number" && Number.isFinite(value)) {
const normalized = Math.floor(value)
return normalized > 0 ? normalized : null
return normalized >= 0 ? normalized : null
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
const normalized = Math.floor(parsed)
return normalized > 0 ? normalized : null
return normalized >= 0 ? normalized : null
}
}
return null
}
const normalizePositiveInteger = (value: unknown): number | null => {
const normalized = normalizeNonNegativeInteger(value)
if (normalized === null || normalized <= 0) return null
return normalized
}
const isIntervalUnit = (value: unknown): value is IntervalUnit =>
value === "minute" || value === "day" || value === "month"
export const getDefaultIntervalValue = (): IntervalValue => ({
amount: 1,
unit: DEFAULT_INTERVAL_UNIT,
days: 1,
hours: 0,
minutes: 0,
})
const fromTotalMinutes = (totalMinutes: number): IntervalValue => {
const normalized = Math.max(0, Math.floor(totalMinutes))
const days = Math.floor(normalized / (24 * 60))
const hours = Math.floor((normalized % (24 * 60)) / 60)
const minutes = normalized % 60
return { days, hours, minutes }
}
const toTotalMinutes = (value: IntervalValue): number =>
value.days * 24 * 60 + value.hours * 60 + value.minutes
const normalizeCompositeValue = (value: Partial<IntervalValue>): IntervalValue | null => {
const days = normalizeNonNegativeInteger(value.days) ?? 0
const hours = normalizeNonNegativeInteger(value.hours) ?? 0
const minutes = normalizeNonNegativeInteger(value.minutes) ?? 0
const totalMinutes = days * 24 * 60 + hours * 60 + minutes
if (totalMinutes <= 0) return null
return fromTotalMinutes(totalMinutes)
}
const legacyToComposite = (value: Partial<LegacyIntervalValue>): IntervalValue | null => {
const amount = normalizePositiveInteger(value.amount)
if (!amount || !isIntervalUnit(value.unit)) return null
if (value.unit === "minute") return fromTotalMinutes(amount)
if (value.unit === "day") return fromTotalMinutes(amount * 24 * 60)
// Legacy month values are approximated as 30 days in the editor.
return fromTotalMinutes(amount * 30 * 24 * 60)
}
export const parseIntervalTriggerValue = (value: unknown): IntervalValue | null => {
if (typeof value === "object" && value !== null) {
const amount = normalizePositiveInteger((value as IntervalValue).amount)
const unit = (value as IntervalValue).unit
if (amount && isIntervalUnit(unit)) {
return { amount, unit }
}
const composite = normalizeCompositeValue(value as Partial<IntervalValue>)
if (composite) return composite
const legacy = legacyToComposite(value as Partial<LegacyIntervalValue>)
if (legacy) return legacy
}
const text = value === null || value === undefined ? "" : String(value).trim()
@@ -46,15 +89,16 @@ export const parseIntervalTriggerValue = (value: unknown): IntervalValue | null
const legacyMinutes = normalizePositiveInteger(text)
if (legacyMinutes !== null && !text.startsWith("{")) {
return { amount: legacyMinutes, unit: "minute" }
return fromTotalMinutes(legacyMinutes)
}
try {
const parsed = JSON.parse(text) as Partial<IntervalValue>
const amount = normalizePositiveInteger(parsed.amount)
if (amount && isIntervalUnit(parsed.unit)) {
return { amount, unit: parsed.unit }
}
const parsed = JSON.parse(text) as Partial<IntervalValue & LegacyIntervalValue>
const composite = normalizeCompositeValue(parsed)
if (composite) return composite
const legacy = legacyToComposite(parsed)
if (legacy) return legacy
} catch {
void 0
}
@@ -63,17 +107,15 @@ export const parseIntervalTriggerValue = (value: unknown): IntervalValue | null
}
export const stringifyIntervalTriggerValue = (value: IntervalValue): string | null => {
const amount = normalizePositiveInteger(value.amount)
if (!amount || !isIntervalUnit(value.unit)) {
return null
}
const normalized = normalizeCompositeValue(value)
if (!normalized) return null
if (value.unit === "minute") {
return String(amount)
}
const totalMinutes = toTotalMinutes(normalized)
if (totalMinutes <= 0) return null
return JSON.stringify({
amount,
unit: value.unit,
days: normalized.days,
hours: normalized.hours,
minutes: normalized.minutes,
})
}
@@ -1,116 +1,87 @@
import React, { useMemo } from "react"
import { InputNumber, Select } from "antd"
import React from "react"
import { InputNumber } from "antd"
import type { WidgetProps } from "@react-awesome-query-builder/antd"
import { useTranslation } from "react-i18next"
import {
getDefaultIntervalValue,
parseIntervalTriggerValue,
stringifyIntervalTriggerValue,
type IntervalValue,
} from "./IntervalValueCodec"
export type IntervalUnit = "month" | "day" | "minute"
export { parseIntervalTriggerValue, stringifyIntervalTriggerValue } from "./IntervalValueCodec"
export type { IntervalValue, IntervalUnit } from "./IntervalValueCodec"
export interface IntervalValue {
amount: number
unit: IntervalUnit
}
export const DEFAULT_INTERVAL_UNIT: IntervalUnit = "day"
export const getDefaultIntervalValue = (): IntervalValue => ({
amount: 1,
unit: DEFAULT_INTERVAL_UNIT,
})
export const parseIntervalTriggerValue = (value: unknown): IntervalValue | null => {
if (value === null || value === undefined) return null
const str = String(value).trim()
if (!str) return null
try {
const parsed = JSON.parse(str)
if (
typeof parsed === "object" &&
parsed !== null &&
typeof parsed.amount === "number" &&
typeof parsed.unit === "string"
) {
const validUnits: IntervalUnit[] = ["month", "day", "minute"]
if (validUnits.includes(parsed.unit as IntervalUnit)) {
return {
amount: Math.max(1, Math.floor(parsed.amount)),
unit: parsed.unit as IntervalUnit,
}
}
}
} catch {
void 0
const buildNextIntervalValue = (
currentValue: IntervalValue | null,
patch: Partial<IntervalValue>
): IntervalValue => {
const base = currentValue ?? getDefaultIntervalValue()
return {
days: patch.days ?? base.days,
hours: patch.hours ?? base.hours,
minutes: patch.minutes ?? base.minutes,
}
return null
}
export const stringifyIntervalTriggerValue = (value: IntervalValue): string => {
return JSON.stringify({
amount: value.amount,
unit: value.unit,
})
}
const getDisplayValue = (value: number | undefined, fallback = 0) =>
typeof value === "number" && Number.isFinite(value) ? value : fallback
export const IntervalValueWidget: React.FC<WidgetProps> = ({
value,
setValue,
readonly,
placeholder,
}) => {
const { t } = useTranslation()
const parsedValue = parseIntervalTriggerValue(value)
const unitOptions = useMemo(
() => [
{ label: t("autoScore.intervalUnitMonth"), value: "month" },
{ label: t("autoScore.intervalUnitDay"), value: "day" },
{ label: t("autoScore.intervalUnitMinute"), value: "minute" },
],
[t]
)
const handleAmountChange = (nextAmount: number | null) => {
if (nextAmount === null) {
setValue(null)
return
}
const serializedValue = stringifyIntervalTriggerValue({
amount: nextAmount,
unit: parsedValue?.unit ?? DEFAULT_INTERVAL_UNIT,
})
setValue(serializedValue)
}
const handleUnitChange = (nextUnit: IntervalUnit) => {
const baseValue = parsedValue ?? getDefaultIntervalValue()
const serializedValue = stringifyIntervalTriggerValue({
amount: baseValue.amount,
unit: nextUnit,
})
setValue(serializedValue)
const updateValue = (patch: Partial<IntervalValue>) => {
const nextValue = buildNextIntervalValue(parsedValue, patch)
const serialized = stringifyIntervalTriggerValue(nextValue)
setValue(serialized)
}
return (
<div style={{ display: "flex", gap: 8, minWidth: 280 }}>
<InputNumber
min={1}
precision={0}
disabled={readonly}
value={parsedValue?.amount}
placeholder={placeholder || t("autoScore.intervalAmountPlaceholder")}
onChange={handleAmountChange}
style={{ width: 130 }}
/>
<Select
disabled={readonly}
value={parsedValue?.unit ?? DEFAULT_INTERVAL_UNIT}
options={unitOptions}
onChange={handleUnitChange}
style={{ width: 140 }}
/>
<div style={{ display: "flex", gap: 12, minWidth: 360 }}>
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
{t("autoScore.intervalPartDay")}
</span>
<InputNumber
min={0}
precision={0}
disabled={readonly}
value={getDisplayValue(parsedValue?.days)}
onChange={(next) => updateValue({ days: Math.max(0, Number(next ?? 0)) })}
style={{ width: "100%" }}
/>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
{t("autoScore.intervalPartHour")}
</span>
<InputNumber
min={0}
precision={0}
disabled={readonly}
value={getDisplayValue(parsedValue?.hours)}
onChange={(next) => updateValue({ hours: Math.max(0, Number(next ?? 0)) })}
style={{ width: "100%" }}
/>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
{t("autoScore.intervalPartMinute")}
</span>
<InputNumber
min={0}
precision={0}
disabled={readonly}
value={getDisplayValue(parsedValue?.minutes)}
onChange={(next) => updateValue({ minutes: Math.max(0, Number(next ?? 0)) })}
style={{ width: "100%" }}
/>
</div>
</div>
)
}
@@ -1,5 +1,5 @@
import React, { useMemo } from "react"
import { Card } from "antd"
import { Alert, Card } from "antd"
import {
Builder,
Query,
@@ -46,6 +46,12 @@ export const TriggerRuleBuilder: React.FC<TriggerRuleBuilderProps> = ({
style={{ marginBottom: "24px", backgroundColor: "var(--ss-card-bg)" }}
title={t("autoScore.whenTriggered")}
>
<Alert
type="info"
showIcon
style={{ marginBottom: "16px" }}
title={t("autoScore.triggerScheduleHint")}
/>
<div
className="query-builder-container"
style={{
+59 -10
View File
@@ -22,6 +22,7 @@ import type { ColumnsType } from "antd/es/table"
import { useTranslation } from "react-i18next"
import { fetchAllTags } from "./TagEditorDialog"
import { ActionEditor } from "./AutoScore/ActionEditor"
import { parseIntervalTriggerValue } from "./AutoScore/IntervalValueCodec"
import { TriggerRuleBuilder } from "./AutoScore/TriggerRuleBuilder"
import {
actionDraftsToPayload,
@@ -29,11 +30,11 @@ import {
createDefaultActionDraft,
createEmptyTriggerTree,
createTriggerQueryConfig,
hasUnsupportedTriggerLogic,
normalizeTriggerTree,
queryTreeToJson,
normalizeActionDrafts,
queryTreeToTriggers,
triggersToQueryTree,
triggerTreeJsonToQueryTree,
type ActionDraft,
type AutoScoreExecutionBatch,
type AutoScoreExecutionConfig,
@@ -203,16 +204,16 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
return
}
if (hasUnsupportedTriggerLogic(triggerTree, triggerConfig)) {
messageApi.warning(t("autoScore.unsupportedLogic"))
return
}
const triggers = queryTreeToTriggers(triggerTree, triggerConfig)
const triggerTreeJson = queryTreeToJson(triggerTree, triggerConfig)
if (triggers.length === 0) {
messageApi.warning(t("autoScore.triggerRequired"))
return
}
if (!triggers.some((trigger) => trigger.event === "interval_time_passed")) {
messageApi.warning(t("autoScore.intervalTriggerRequired"))
return
}
const normalizedActionDrafts = normalizeActionDrafts(actionDrafts)
const actionPayload = actionDraftsToPayload(normalizedActionDrafts)
@@ -243,6 +244,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
enabled: true,
studentNames,
triggers,
triggerTree: triggerTreeJson,
actions: actionPayload.actions,
execution,
}
@@ -281,7 +283,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
studentNames: rule.studentNames || [],
execution: rule.execution || {},
})
setTriggerTree(triggersToQueryTree(triggerConfig, rule.triggers || []))
setTriggerTree(triggerTreeJsonToQueryTree(triggerConfig, rule.triggerTree, rule.triggers || []))
setActionDrafts(actionsToDrafts(rule.actions || []))
}
@@ -342,6 +344,7 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
enabled: rule.enabled,
studentNames: rule.studentNames,
triggers: rule.triggers,
triggerTree: rule.triggerTree ?? null,
actions: rule.actions,
execution: rule.execution || {},
lastExecuted: rule.lastExecuted ?? null,
@@ -370,6 +373,44 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
return date.toLocaleString()
}
const formatTriggerSummary = (trigger: AutoScoreRule["triggers"][number]) => {
if (trigger.event === "interval_time_passed") {
const intervalValue = parseIntervalTriggerValue(trigger.value)
if (!intervalValue) {
return `${t("autoScore.triggerIntervalTime")}: -`
}
return `${t("autoScore.triggerIntervalTime")}: ${intervalValue.days}${t(
"autoScore.intervalPartDay"
)} ${intervalValue.hours}${t("autoScore.intervalPartHour")} ${intervalValue.minutes}${t(
"autoScore.intervalPartMinute"
)}`
}
if (trigger.event === "student_has_tag") {
return `${t("autoScore.triggerStudentTag")}: ${String(trigger.value || "").trim() || "-"}`
}
if (
trigger.event === "query_sql" ||
trigger.event === "student_query_sql" ||
trigger.event === "student_sql"
) {
return `${t("autoScore.triggerStudentSql")}: ${String(trigger.value || "").trim() || "-"}`
}
return `${trigger.event}: ${String(trigger.value || "").trim() || "-"}`
}
const formatActionSummary = (action: AutoScoreRule["actions"][number]) => {
if (action.event === "add_score") {
return `${t("autoScore.actionAddScore")}: ${String(action.value || "").trim() || "-"}`
}
if (action.event === "add_tag") {
return `${t("autoScore.actionAddTag")}: ${String(action.value || "").trim() || "-"}`
}
if (action.event === "settle_score") {
return `${t("autoScore.actionSettleScore")}: ${t("autoScore.actionSettleScoreHint")}`
}
return `${action.event}: ${String(action.value || "").trim() || "-"}`
}
const handleRollbackBatch = async (batchId: string) => {
const api = (window as any).api
if (!api || !canEdit) return
@@ -434,13 +475,21 @@ function AutoScoreManager({ canEdit }: AutoScoreManagerProps): React.JSX.Element
title: t("autoScore.triggers"),
key: "triggers",
width: 120,
render: (_, row) => <Tag>{t("autoScore.triggerCount", { count: row.triggers.length })}</Tag>,
render: (_, row) => (
<Tooltip title={row.triggers.map(formatTriggerSummary).join("\n") || "-"}>
<Tag>{t("autoScore.triggerCount", { count: row.triggers.length })}</Tag>
</Tooltip>
),
},
{
title: t("autoScore.actions"),
key: "actions",
width: 120,
render: (_, row) => <Tag>{t("autoScore.actionCount", { count: row.actions.length })}</Tag>,
render: (_, row) => (
<Tooltip title={row.actions.map(formatActionSummary).join("\n") || "-"}>
<Tag>{t("autoScore.actionCount", { count: row.actions.length })}</Tag>
</Tooltip>
),
},
{
title: t("autoScore.lastExecuted"),
+12 -10
View File
@@ -274,17 +274,19 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
<h2 style={{ marginBottom: "24px", color: "var(--ss-text-main)" }}>{t("score.title")}</h2>
<Card style={{ marginBottom: "24px", backgroundColor: "var(--ss-card-bg)" }}>
<Form form={form} layout="vertical" initialValues={{ type: "add" }}>
<Form form={form} layout="vertical" initialValues={{ type: "add", student_name: [] }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "24px" }}>
<Form.Item label={t("score.student")} name="student_name">
<Space direction="vertical" size={8} style={{ width: "100%" }}>
<Select
mode="multiple"
showSearch
placeholder={t("score.pleaseSelectStudent")}
filterOption={(input, option) => matchStudentName(getOptionLabel(option), input)}
options={students.map((s) => ({ label: s.name, value: s.name }))}
/>
<Form.Item label={t("score.student")}>
<Space orientation="vertical" size={8} style={{ width: "100%" }}>
<Form.Item name="student_name" noStyle>
<Select
mode="multiple"
showSearch
placeholder={t("score.pleaseSelectStudent")}
filterOption={(input, option) => matchStudentName(getOptionLabel(option), input)}
options={students.map((s) => ({ label: s.name, value: s.name }))}
/>
</Form.Item>
<Space size={8} wrap>
<Button
size="small"
+5
View File
@@ -772,6 +772,7 @@
"addTrigger": "Add Rule",
"addAction": "Add Action",
"whenTriggered": "When the following rules trigger",
"triggerScheduleHint": "Note: whether a rule runs is still scheduled by the interval trigger. Tags, SQL, and AND / OR / NOT only decide which students are matched for that run.",
"triggeredActions": "Actions triggered when rules are met",
"addAutomation": "Add Automation",
"updateAutomation": "Update Automation",
@@ -785,6 +786,7 @@
"fetchFailed": "Failed to get automation",
"unsupportedLogic": "Saving currently supports AND-only conditions. OR / NOT logic is not wired to backend persistence yet",
"triggerRequired": "Please add at least one trigger",
"intervalTriggerRequired": "Please add at least one interval trigger, otherwise the rule will never run automatically",
"actionRequired": "Please add at least one action",
"createSuccess": "Automation created successfully",
"updateSuccess": "Automation updated successfully",
@@ -841,6 +843,9 @@
"intervalUnitMonth": "month(s) later",
"intervalUnitDay": "day(s) later",
"intervalUnitMinute": "minute(s) later",
"intervalPartDay": "Days",
"intervalPartHour": "Hours",
"intervalPartMinute": "Minutes",
"minutesPlaceholder": "Enter minutes",
"minutes": "minutes",
"tagsPlaceholder": "Enter tag name",
+6 -1
View File
@@ -772,6 +772,7 @@
"addTrigger": "添加规则",
"addAction": "添加行动",
"whenTriggered": "当以下规则触发时",
"triggerScheduleHint": "提示:规则是否执行仍由“间隔时间”触发器决定,标签、SQL、AND / OR / NOT 仅用于筛选本次命中的学生。",
"triggeredActions": "满足规则时触发的行动",
"addAutomation": "添加自动化",
"updateAutomation": "更新自动化",
@@ -785,6 +786,7 @@
"fetchFailed": "获取自动化失败",
"unsupportedLogic": "当前版本保存规则时仅支持 AND 条件,OR / NOT 逻辑暂未接入后端存储",
"triggerRequired": "请至少添加一个触发器",
"intervalTriggerRequired": "请至少添加一个“间隔时间”触发器,否则规则不会自动执行",
"actionRequired": "请至少添加一个行动",
"createSuccess": "自动化创建成功",
"updateSuccess": "自动化更新成功",
@@ -838,7 +840,10 @@
"intervalAmountPlaceholder": "请输入间隔时间",
"intervalUnitMonth": "个月",
"intervalUnitDay": "天",
"intervalUnitMinute": "分 钟",
"intervalUnitMinute": "分钟",
"intervalPartDay": "天",
"intervalPartHour": "小时",
"intervalPartMinute": "分钟",
"minutesPlaceholder": "请输入分钟数",
"minutes": "分钟",
"tagsPlaceholder": "请输入标签名称",
+26 -6
View File
@@ -59,6 +59,7 @@ export interface autoScoreRule {
enabled: boolean
studentNames: string[]
triggers: autoScoreTrigger[]
triggerTree?: any | null
actions: autoScoreAction[]
execution?: autoScoreExecutionConfig
lastExecuted?: string | null
@@ -255,17 +256,34 @@ const api = {
queryEvents: (params?: { limit?: number }): Promise<{ success: boolean; data: any[] }> =>
invoke("event_query", { params }),
createEvent: (data: {
studentName: string
reasonContent: string
student_name?: string
reason_content?: string
delta: number
}): Promise<{ success: boolean; data?: number; message?: string }> =>
invoke("event_create", { data }),
studentName?: string
reasonContent?: string
}): Promise<{ success: boolean; data?: number; message?: string }> => {
const normalized = {
student_name: String(data.student_name ?? data.studentName ?? "").trim(),
reason_content: String(data.reason_content ?? data.reasonContent ?? "").trim(),
delta: Number(data.delta),
}
return invoke("event_create", { data: normalized })
},
deleteEvent: (uuid: string): Promise<{ success: boolean }> => invoke("event_delete", { uuid }),
queryEventsByStudent: (params: {
studentName: string
student_name?: string
limit?: number
start_time?: string
studentName?: string
startTime?: string
}): Promise<{ success: boolean; data: any[] }> => invoke("event_query_by_student", { params }),
}): Promise<{ success: boolean; data: any[] }> =>
invoke("event_query_by_student", {
params: {
student_name: String(params.student_name ?? params.studentName ?? "").trim(),
limit: params.limit,
start_time: params.start_time ?? params.startTime,
},
}),
queryLeaderboard: (params: {
range: "today" | "week" | "month"
}): Promise<{ success: boolean; data: { startTime: string; rows: any[] } }> =>
@@ -298,6 +316,7 @@ const api = {
enabled: boolean
studentNames: string[]
triggers: autoScoreTrigger[]
triggerTree?: any | null
actions: autoScoreAction[]
execution?: autoScoreExecutionConfig
}): Promise<{ success: boolean; data?: number; message?: string }> =>
@@ -308,6 +327,7 @@ const api = {
enabled: boolean
studentNames: string[]
triggers: autoScoreTrigger[]
triggerTree?: any | null
actions: autoScoreAction[]
execution?: autoScoreExecutionConfig
}): Promise<{ success: boolean; data?: boolean; message?: string }> =>