diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 7288008..2843090 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/app/app.vue b/app/app.vue index 49c748e..71844c0 100644 --- a/app/app.vue +++ b/app/app.vue @@ -72,7 +72,7 @@ const scheduleHotjarInitialization = () => { hotjarInitialized.value = true; window.setTimeout(() => { - initialize(HOTJAR_ID, HOTJAR_SCRIPT_VERSION); + initialize(); }, HOTJAR_INIT_DELAY); }; diff --git a/app/components/editor/DecisionNodeCanvas.vue b/app/components/editor/DecisionNodeCanvas.vue index 14c08b6..28a8189 100644 --- a/app/components/editor/DecisionNodeCanvas.vue +++ b/app/components/editor/DecisionNodeCanvas.vue @@ -214,7 +214,7 @@ import type { DecisionNodeModel, DecisionNodeTransition, DecisionNodeLayout, -} from '~/shared/types/decision' +} from '~~/shared/types/decision' const NODE_WIDTH = 280 const NODE_HEIGHT = 160 @@ -403,14 +403,22 @@ const preparedNodes = computed(() => { const width = node.layout?.width ?? NODE_WIDTH const height = node.layout?.height ?? NODE_HEIGHT const accent = node.layout?.color || props.roleColors[node.role] || '#22d3ee' - const transitions = (node.model.transitions || []).slice(0, 3).map((transition) => ({ + const transitions = (node.model.transitions || []).slice(0, 3).map((transition: DecisionNodeTransition) => ({ key: transition.key || `${node.id}_${transition.target}_${transition.type}`, target: transition.target, type: transition.type, class: transitionClass(transition), })) - const baseLayout: DecisionNodeLayout = { ...(node.layout || {}) } + const baseLayout: DecisionNodeLayout = { + x: node.layout?.x ?? 0, + y: node.layout?.y ?? 0, + width: node.layout?.width, + height: node.layout?.height, + color: node.layout?.color, + icon: node.layout?.icon, + locked: node.layout?.locked, + } const displayLayout: DecisionNodeLayout = { ...baseLayout, x: (baseLayout.x ?? 0) + offset.x, @@ -430,6 +438,8 @@ const preparedNodes = computed(() => { }) }) +type PreparedNode = typeof preparedNodes.value[number] + const canvasBounds = computed(() => { const defaultWidth = MIN_WORKSPACE_WIDTH - WORKSPACE_PADDING const defaultHeight = MIN_WORKSPACE_HEIGHT - WORKSPACE_PADDING @@ -533,7 +543,7 @@ const edges = computed(() => { }) const color = transitionColor(transition) const dashed = Boolean(transition.autoTrigger || transition.type === 'auto') - const highlighted = node.selected || source.highlighted + const highlighted = Boolean(node.selected || source.highlighted) const key = transition.key || `${node.id}_${transition.target}_${transition.type}` lines.push({ @@ -715,11 +725,12 @@ function buildSmoothPath(points: PathPoint[], radius = 32) { return '' } - const commands: string[] = [`M ${points[0].x} ${points[0].y}`] + const first = points[0]! + const commands: string[] = [`M ${first.x} ${first.y}`] for (let i = 1; i < points.length; i += 1) { - const prev = points[i - 1] - const curr = points[i] + const prev = points[i - 1]! + const curr = points[i]! const next = points[i + 1] if (!next) { @@ -799,7 +810,7 @@ interface DragState { let dragState: DragState | null = null -function onNodePointerDown(event: PointerEvent, node: ReturnType) { +function onNodePointerDown(event: PointerEvent, node: PreparedNode) { if (event.button !== 0) return emit('select', node.id) emit('node-drag-start', node.id) @@ -926,7 +937,7 @@ function onWheel(event: WheelEvent) { emit('update:pan', { x: nextPanX, y: nextPanY }) } -function nodeStyle(node: ReturnType) { +function nodeStyle(node: PreparedNode) { return { width: `${node.width}px`, height: `${node.height}px`, diff --git a/app/pages/admin/index.vue b/app/pages/admin/index.vue index 191f49d..f350cc5 100644 --- a/app/pages/admin/index.vue +++ b/app/pages/admin/index.vue @@ -1085,9 +1085,51 @@ interface DecisionTraceCall { error?: string } +interface DecisionTraceCandidate { + id: string + flow?: string + summary?: string +} + +interface DecisionTraceEliminationContext { + patterns?: Array<{ id?: string; pattern?: string; flags?: string }> + condition?: { + id?: string + type: 'variable_value' | 'regex' | 'regex_not' + variable?: string + operator?: string + value?: unknown + pattern?: string + patternFlags?: string + } + actualValue?: unknown + expectedValue?: unknown +} + +interface DecisionTraceElimination { + candidate: DecisionTraceCandidate + kind: 'regex' | 'condition' + reason: string + context?: DecisionTraceEliminationContext +} + +interface DecisionTraceStep { + stage: string + label: string + candidates: DecisionTraceCandidate[] + eliminated?: DecisionTraceElimination[] + note?: string +} + interface DecisionTraceMetadata { calls?: DecisionTraceCall[] fallback?: { used?: boolean; reason?: string; selected?: string } + candidateTimeline?: { + steps: DecisionTraceStep[] + fallbackUsed?: boolean + autoSelected?: DecisionTraceCandidate | null + } + autoSelection?: { id: string; flow: string; reason?: string } } interface CandidateSnapshot { @@ -1122,6 +1164,7 @@ interface TransmissionMetadata { controller_say_tpl?: string off_schema?: boolean radio_check?: boolean + activate_flow?: string | { slug: string; mode?: string } } decisionTrace?: DecisionTraceMetadata context?: TransmissionContextSnapshot @@ -1410,6 +1453,32 @@ function describeTransition(transition: any) { return details.length ? `${destination} (${details.join(', ')})` : destination } +function describeElimination(entry: DecisionTraceElimination | null | undefined): string { + if (!entry || typeof entry !== 'object') { + return '' + } + if (entry.kind === 'regex' && entry.context?.patterns?.length) { + const patterns = entry.context.patterns + .map((pattern) => pattern?.pattern) + .filter((value): value is string => Boolean(value)) + .join(', ') + return patterns ? `Patterns: ${patterns}` : entry.reason + } + if (entry.kind === 'condition' && entry.context?.condition) { + const condition = entry.context.condition + if (condition.type === 'regex' || condition.type === 'regex_not') { + const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : '' + return flag ? `Condition: ${condition.type} ${flag}` : entry.reason + } + const variable = condition.variable || 'value' + const operator = condition.operator || '==' + const expected = entry.context?.expectedValue ?? condition.value ?? '—' + const actual = entry.context?.actualValue ?? '—' + return `${variable} ${operator} ${expected} (actual: ${actual})` + } + return entry.reason +} + function isExpired(expiresAt?: string) { if (!expiresAt) return false const date = new Date(expiresAt) diff --git a/app/pages/classroom.vue b/app/pages/classroom.vue index 5284b96..d0ed58b 100644 --- a/app/pages/classroom.vue +++ b/app/pages/classroom.vue @@ -1045,7 +1045,7 @@ :key="segment.type === 'field' ? `f-${segment.key}` : `t-${idx}`"> {{ - displayCallsign(typeof segment.text === 'function' && scenario ? segment.text(scenario) : segment.text) + displayCallsign(typeof segment.text === 'function' ? (scenario ? segment.text(scenario) : '') : segment.text) }}