refactor(worksheet-parsing): remove backward-compatible prop fallbacks
Components now require WorksheetParsingContext instead of accepting optional streaming props as fallbacks. This simplifies the codebase by removing ~300 lines of fallback logic. Changes: - Create MockWorksheetParsingProvider for Storybook/testing - Simplify OfflineWorkSection to use required context - Simplify PhotoViewerEditor to use required context (~100 lines removed) - Update stories to wrap with mock provider - Remove unused StreamingParseState/StreamingReparseState types 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e544688d25
commit
23f79802e8
|
|
@ -653,7 +653,6 @@ export function SummaryClient({
|
|||
)?.corrections?.[0]?.problemNumber ?? null)
|
||||
: null
|
||||
}
|
||||
reparsingPhotoId={getPendingAttachmentId(reparseSelected)}
|
||||
onCancelParsing={(attachmentId) => cancelParsing.mutate(attachmentId)}
|
||||
onApproveProblem={async (photoId, problemIndex) => {
|
||||
await updateReviewProgress.mutateAsync({
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { RefObject } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { ParsingStatus } from "@/db/schema/practice-attachments";
|
||||
import { useWorksheetParsingContextOptional } from "@/contexts/WorksheetParsingContext";
|
||||
import { useWorksheetParsingContext } from "@/contexts/WorksheetParsingContext";
|
||||
import { api } from "@/lib/queryClient";
|
||||
import type {
|
||||
WorksheetParsingResult,
|
||||
|
|
@ -12,7 +12,6 @@ import type {
|
|||
} from "@/lib/worksheet-parsing";
|
||||
import { css } from "../../../styled-system/css";
|
||||
import { WorksheetReviewSummary } from "../worksheet-parsing";
|
||||
import type { StreamingParseState } from "@/hooks/useWorksheetParsing";
|
||||
import { ParsingProgressOverlay } from "./ParsingProgressOverlay";
|
||||
import { ParsingProgressPanel } from "./ParsingProgressPanel";
|
||||
import { ProgressiveHighlightOverlayCompact } from "./ProgressiveHighlightOverlay";
|
||||
|
|
@ -82,17 +81,6 @@ export interface OfflineWorkSectionProps {
|
|||
onUnapprove?: (attachmentId: string) => Promise<void>;
|
||||
/** ID of attachment currently being unapproved */
|
||||
unaprovingId?: string | null;
|
||||
// === Streaming parsing props ===
|
||||
/** Streaming parsing state (when using streaming mode) */
|
||||
streamingState?: StreamingParseState | null;
|
||||
/** ID of attachment currently being streamed */
|
||||
streamingAttachmentId?: string | null;
|
||||
/** Cancel streaming parsing */
|
||||
onCancelStreaming?: () => void;
|
||||
/** Toggle expanded state for streaming panel */
|
||||
isStreamingPanelExpanded?: boolean;
|
||||
/** Callback to toggle streaming panel */
|
||||
onToggleStreamingPanel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,130 +121,59 @@ export function OfflineWorkSection({
|
|||
approvingId = null,
|
||||
onUnapprove,
|
||||
unaprovingId = null,
|
||||
// Streaming props
|
||||
streamingState = null,
|
||||
streamingAttachmentId = null,
|
||||
onCancelStreaming,
|
||||
isStreamingPanelExpanded = false,
|
||||
onToggleStreamingPanel,
|
||||
}: OfflineWorkSectionProps) {
|
||||
// ============================================================================
|
||||
// Context Integration (optional - falls back to props if not in provider)
|
||||
// Context Integration (required - must be wrapped in WorksheetParsingProvider)
|
||||
// ============================================================================
|
||||
const parsingContext = useWorksheetParsingContextOptional();
|
||||
const parsingContext = useWorksheetParsingContext();
|
||||
|
||||
// Local state for streaming panel expansion (used when context is available)
|
||||
const [localIsStreamingPanelExpanded, setLocalIsStreamingPanelExpanded] =
|
||||
// Local state for streaming panel expansion
|
||||
const [isStreamingPanelExpanded, setIsStreamingPanelExpanded] =
|
||||
useState(false);
|
||||
|
||||
// Derive effective streaming state from context or props
|
||||
const effectiveStreamingState = useMemo<StreamingParseState | null>(() => {
|
||||
if (parsingContext?.state.streaming) {
|
||||
// Map context state to StreamingParseState format for backward compat
|
||||
const { streaming } = parsingContext.state;
|
||||
// Map statuses that don't exist in StreamingParseState
|
||||
let mappedStatus: StreamingParseState["status"] = streaming.status as StreamingParseState["status"];
|
||||
if (streaming.status === "cancelled") {
|
||||
mappedStatus = "idle";
|
||||
} else if (streaming.status === "processing") {
|
||||
mappedStatus = "generating"; // processing is similar to generating
|
||||
}
|
||||
return {
|
||||
status: mappedStatus,
|
||||
reasoningText: streaming.reasoningText,
|
||||
outputText: streaming.outputText,
|
||||
error: null,
|
||||
progressMessage: streaming.progressMessage,
|
||||
result: null, // Result is in context state
|
||||
stats: null,
|
||||
completedProblems: streaming.completedProblems,
|
||||
};
|
||||
}
|
||||
return streamingState;
|
||||
}, [parsingContext?.state.streaming, streamingState]);
|
||||
|
||||
const effectiveStreamingAttachmentId = useMemo(() => {
|
||||
if (parsingContext) {
|
||||
return parsingContext.state.activeAttachmentId;
|
||||
}
|
||||
return streamingAttachmentId;
|
||||
}, [parsingContext, streamingAttachmentId]);
|
||||
|
||||
// Effective handlers - use context if available, otherwise props
|
||||
// Handlers that delegate to context
|
||||
const handleParse = useCallback(
|
||||
(attachmentId: string) => {
|
||||
if (parsingContext) {
|
||||
parsingContext.startParse({ attachmentId });
|
||||
} else if (onParse) {
|
||||
onParse(attachmentId);
|
||||
}
|
||||
parsingContext.startParse({ attachmentId });
|
||||
},
|
||||
[parsingContext, onParse],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
const handleCancelStreaming = useCallback(() => {
|
||||
if (parsingContext) {
|
||||
parsingContext.cancel();
|
||||
} else if (onCancelStreaming) {
|
||||
onCancelStreaming();
|
||||
}
|
||||
}, [parsingContext, onCancelStreaming]);
|
||||
parsingContext.cancel();
|
||||
}, [parsingContext]);
|
||||
|
||||
const handleApproveAll = useCallback(
|
||||
async (attachmentId: string) => {
|
||||
if (parsingContext) {
|
||||
await parsingContext.approve(attachmentId);
|
||||
} else if (onApproveAll) {
|
||||
await onApproveAll(attachmentId);
|
||||
}
|
||||
await parsingContext.approve(attachmentId);
|
||||
},
|
||||
[parsingContext, onApproveAll],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
const handleUnapprove = useCallback(
|
||||
async (attachmentId: string) => {
|
||||
if (parsingContext) {
|
||||
await parsingContext.unapprove(attachmentId);
|
||||
} else if (onUnapprove) {
|
||||
await onUnapprove(attachmentId);
|
||||
}
|
||||
await parsingContext.unapprove(attachmentId);
|
||||
},
|
||||
[parsingContext, onUnapprove],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
// Effective panel expansion state - use local state when context available, otherwise props
|
||||
const effectiveIsStreamingPanelExpanded = parsingContext
|
||||
? localIsStreamingPanelExpanded
|
||||
: isStreamingPanelExpanded;
|
||||
|
||||
// Effective toggle handler - use local setter when context available, otherwise prop
|
||||
const handleToggleStreamingPanel = useCallback(() => {
|
||||
if (parsingContext) {
|
||||
setLocalIsStreamingPanelExpanded((prev) => !prev);
|
||||
} else if (onToggleStreamingPanel) {
|
||||
onToggleStreamingPanel();
|
||||
}
|
||||
}, [parsingContext, onToggleStreamingPanel]);
|
||||
setIsStreamingPanelExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Reset local panel expansion when streaming completes
|
||||
// Reset panel expansion when streaming completes
|
||||
useEffect(() => {
|
||||
if (
|
||||
parsingContext?.state.streaming?.status === "complete" ||
|
||||
parsingContext?.state.streaming?.status === "error" ||
|
||||
parsingContext?.state.streaming?.status === "cancelled"
|
||||
) {
|
||||
// Reset expansion after streaming ends
|
||||
const status = parsingContext.state.streaming?.status;
|
||||
if (status === "complete" || status === "error" || status === "cancelled") {
|
||||
const timer = setTimeout(() => {
|
||||
setLocalIsStreamingPanelExpanded(false);
|
||||
setIsStreamingPanelExpanded(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [parsingContext?.state.streaming?.status]);
|
||||
}, [parsingContext.state.streaming?.status]);
|
||||
|
||||
// Check if any parsing operation is active (via context or props)
|
||||
const isParsingActive = parsingContext
|
||||
? parsingContext.isAnyParsingActive()
|
||||
: streamingAttachmentId !== null;
|
||||
// Check if any parsing operation is active
|
||||
const isParsingActive = parsingContext.isAnyParsingActive();
|
||||
|
||||
// ============================================================================
|
||||
// Original Component Logic
|
||||
|
|
@ -426,13 +343,14 @@ export function OfflineWorkSection({
|
|||
{/* Existing photos */}
|
||||
{attachments.map((att, index) => {
|
||||
// Check if this tile is currently streaming
|
||||
const streamingState = parsingContext.state.streaming;
|
||||
const isStreaming =
|
||||
effectiveStreamingAttachmentId === att.id && effectiveStreamingState !== null;
|
||||
parsingContext.state.activeAttachmentId === att.id && streamingState !== null;
|
||||
const streamingActive =
|
||||
isStreaming &&
|
||||
(effectiveStreamingState?.status === "connecting" ||
|
||||
effectiveStreamingState?.status === "reasoning" ||
|
||||
effectiveStreamingState?.status === "generating");
|
||||
(streamingState?.status === "connecting" ||
|
||||
streamingState?.status === "reasoning" ||
|
||||
streamingState?.status === "generating");
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -794,34 +712,32 @@ export function OfflineWorkSection({
|
|||
)}
|
||||
|
||||
{/* Streaming: Progressive highlight overlay */}
|
||||
{streamingActive && effectiveStreamingState && (
|
||||
{streamingActive && streamingState && (
|
||||
<ProgressiveHighlightOverlayCompact
|
||||
completedProblems={effectiveStreamingState.completedProblems}
|
||||
completedProblems={streamingState.completedProblems}
|
||||
staggerDelay={30}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Streaming: Progress overlay */}
|
||||
{streamingActive &&
|
||||
effectiveStreamingState &&
|
||||
(onCancelStreaming || parsingContext) && (
|
||||
<ParsingProgressOverlay
|
||||
progressMessage={effectiveStreamingState.progressMessage}
|
||||
completedCount={effectiveStreamingState.completedProblems.length}
|
||||
isPanelExpanded={effectiveIsStreamingPanelExpanded}
|
||||
onTogglePanel={handleToggleStreamingPanel}
|
||||
onCancel={handleCancelStreaming}
|
||||
hasReasoningText={effectiveStreamingState.reasoningText.length > 0}
|
||||
/>
|
||||
)}
|
||||
{streamingActive && streamingState && (
|
||||
<ParsingProgressOverlay
|
||||
progressMessage={streamingState.progressMessage}
|
||||
completedCount={streamingState.completedProblems.length}
|
||||
isPanelExpanded={isStreamingPanelExpanded}
|
||||
onTogglePanel={handleToggleStreamingPanel}
|
||||
onCancel={handleCancelStreaming}
|
||||
hasReasoningText={streamingState.reasoningText.length > 0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Streaming: Reasoning panel below tile */}
|
||||
{isStreaming && effectiveStreamingState && (
|
||||
{isStreaming && streamingState && (
|
||||
<ParsingProgressPanel
|
||||
isExpanded={effectiveIsStreamingPanelExpanded}
|
||||
reasoningText={effectiveStreamingState.reasoningText}
|
||||
status={effectiveStreamingState.status}
|
||||
isExpanded={isStreamingPanelExpanded}
|
||||
reasoningText={streamingState.reasoningText}
|
||||
status={streamingState.status}
|
||||
isDark={isDark}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,16 @@ export interface ParsingProgressPanelProps {
|
|||
isExpanded: boolean;
|
||||
/** The reasoning text from the LLM */
|
||||
reasoningText: string;
|
||||
/** Current parsing status */
|
||||
/** Current parsing status (from StreamingStatus) */
|
||||
status:
|
||||
| "idle"
|
||||
| "connecting"
|
||||
| "reasoning"
|
||||
| "processing"
|
||||
| "generating"
|
||||
| "complete"
|
||||
| "error"
|
||||
| "idle";
|
||||
| "cancelled";
|
||||
/** Dark mode */
|
||||
isDark?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,9 @@ import {
|
|||
type ProblemCorrection,
|
||||
} from "@/components/worksheet-parsing";
|
||||
import type { ReviewProgress } from "@/lib/worksheet-parsing";
|
||||
import type {
|
||||
StreamingParseState,
|
||||
StreamingReparseState,
|
||||
} from "@/hooks/useWorksheetParsing";
|
||||
import { Z_INDEX } from "@/constants/zIndex";
|
||||
import { useVisualDebug } from "@/contexts/VisualDebugContext";
|
||||
import { useWorksheetParsingContextOptional } from "@/contexts/WorksheetParsingContext";
|
||||
import { useWorksheetParsingContext } from "@/contexts/WorksheetParsingContext";
|
||||
import type {
|
||||
ModelConfig,
|
||||
WorksheetParsingResult,
|
||||
|
|
@ -89,22 +85,6 @@ export interface PhotoViewerEditorProps {
|
|||
corners: Array<{ x: number; y: number }>,
|
||||
rotation: 0 | 90 | 180 | 270,
|
||||
) => Promise<void>;
|
||||
/** Callback to parse worksheet (optional - if not provided, no parse button shown) */
|
||||
onParse?: (
|
||||
photoId: string,
|
||||
modelConfigId?: string,
|
||||
additionalContext?: string,
|
||||
preservedBoundingBoxes?: Record<
|
||||
number,
|
||||
{ x: number; y: number; width: number; height: number }
|
||||
>,
|
||||
) => void;
|
||||
/** ID of the photo currently being parsed (null if none) */
|
||||
parsingPhotoId?: string | null;
|
||||
/** Streaming parse state (null if not streaming) */
|
||||
streamingParseState?: StreamingParseState | null;
|
||||
/** Callback to cancel streaming parse */
|
||||
onCancelStreamingParse?: () => void;
|
||||
/** Available model configurations for parsing */
|
||||
modelConfigs?: ModelConfig[];
|
||||
/** Callback to approve parsed worksheet and create session */
|
||||
|
|
@ -118,24 +98,6 @@ export interface PhotoViewerEditorProps {
|
|||
) => Promise<void>;
|
||||
/** Problem number currently being saved (null if none) */
|
||||
savingProblemNumber?: number | null;
|
||||
/** Callback to re-parse selected problems */
|
||||
onReparseSelected?: (
|
||||
photoId: string,
|
||||
problemIndices: number[],
|
||||
boundingBoxes: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>,
|
||||
additionalContext?: string,
|
||||
) => Promise<void>;
|
||||
/** ID of photo currently being re-parsed (null if none) */
|
||||
reparsingPhotoId?: string | null;
|
||||
/** Streaming re-parse state (null if not streaming) */
|
||||
streamingReparseState?: StreamingReparseState | null;
|
||||
/** Callback to cancel streaming re-parse */
|
||||
onCancelStreamingReparse?: () => void;
|
||||
/** Callback to cancel parsing in progress */
|
||||
onCancelParsing?: (photoId: string) => void;
|
||||
/** Callback when a single problem is approved in focus review mode */
|
||||
|
|
@ -162,19 +124,11 @@ export function PhotoViewerEditor({
|
|||
isOpen,
|
||||
onClose,
|
||||
onEditConfirm,
|
||||
onParse,
|
||||
parsingPhotoId = null,
|
||||
streamingParseState = null,
|
||||
onCancelStreamingParse,
|
||||
modelConfigs = [],
|
||||
onApprove,
|
||||
approvingPhotoId = null,
|
||||
onSubmitCorrection,
|
||||
savingProblemNumber = null,
|
||||
onReparseSelected,
|
||||
reparsingPhotoId = null,
|
||||
streamingReparseState = null,
|
||||
onCancelStreamingReparse,
|
||||
onCancelParsing,
|
||||
onApproveProblem,
|
||||
onFlagProblem,
|
||||
|
|
@ -236,94 +190,23 @@ export function PhotoViewerEditor({
|
|||
cv: opencvRef,
|
||||
} = useDocumentDetection();
|
||||
|
||||
// Try to use context if available (for centralized parsing state management)
|
||||
const parsingContext = useWorksheetParsingContextOptional();
|
||||
// Use the worksheet parsing context (required)
|
||||
const parsingContext = useWorksheetParsingContext();
|
||||
|
||||
// Derive effective streaming state from context or props
|
||||
const effectiveStreamingState = useMemo((): StreamingParseState | null => {
|
||||
if (
|
||||
parsingContext?.state.streaming &&
|
||||
parsingContext.state.streaming.streamType === "initial"
|
||||
) {
|
||||
const { streaming } = parsingContext.state;
|
||||
// Map context status to StreamingParseState status
|
||||
let mappedStatus: StreamingParseState["status"];
|
||||
if (streaming.status === "cancelled") {
|
||||
mappedStatus = "idle";
|
||||
} else if (streaming.status === "processing") {
|
||||
mappedStatus = "generating";
|
||||
} else {
|
||||
mappedStatus = streaming.status as StreamingParseState["status"];
|
||||
}
|
||||
return {
|
||||
status: mappedStatus,
|
||||
reasoningText: streaming.reasoningText ?? "",
|
||||
outputText: streaming.outputText ?? "",
|
||||
error: parsingContext.state.lastError,
|
||||
progressMessage: streaming.progressMessage,
|
||||
result: parsingContext.state.lastResult,
|
||||
// Context uses different ParsingStats type - pass null since stats isn't used in this component
|
||||
stats: null,
|
||||
completedProblems: streaming.completedProblems ?? [],
|
||||
};
|
||||
}
|
||||
return streamingParseState ?? null;
|
||||
}, [parsingContext?.state.streaming, parsingContext?.state.lastError, parsingContext?.state.lastResult, streamingParseState]);
|
||||
// Derive streaming state from context
|
||||
const streamingState = parsingContext.state.streaming;
|
||||
const isInitialParsing =
|
||||
streamingState?.streamType === "initial" &&
|
||||
(streamingState.status === "connecting" ||
|
||||
streamingState.status === "reasoning" ||
|
||||
streamingState.status === "generating");
|
||||
const isReparsing =
|
||||
streamingState?.streamType === "reparse" &&
|
||||
(streamingState.status === "connecting" ||
|
||||
streamingState.status === "processing");
|
||||
const activeAttachmentId = parsingContext.state.activeAttachmentId;
|
||||
|
||||
const effectiveStreamingReparseState = useMemo(
|
||||
(): StreamingReparseState | null => {
|
||||
if (
|
||||
parsingContext?.state.streaming &&
|
||||
parsingContext.state.streaming.streamType === "reparse"
|
||||
) {
|
||||
const { streaming } = parsingContext.state;
|
||||
// Map context status to StreamingReparseState status
|
||||
let mappedStatus: StreamingReparseState["status"];
|
||||
if (streaming.status === "generating" || streaming.status === "reasoning") {
|
||||
mappedStatus = "processing"; // These map to processing in reparse
|
||||
} else {
|
||||
mappedStatus = streaming.status as StreamingReparseState["status"];
|
||||
}
|
||||
return {
|
||||
status: mappedStatus,
|
||||
reasoningText: streaming.reasoningText ?? "",
|
||||
currentProblemIndex: streaming.currentProblemIndex ?? 0,
|
||||
totalProblems: streaming.totalProblems ?? 0,
|
||||
currentProblemWorksheetIndex: null, // Not tracked in context state
|
||||
completedIndices: streaming.completedIndices ?? [],
|
||||
progressMessage: streaming.progressMessage ?? "",
|
||||
error: parsingContext.state.lastError ?? null,
|
||||
result: parsingContext.state.lastResult,
|
||||
};
|
||||
}
|
||||
return streamingReparseState ?? null;
|
||||
},
|
||||
[parsingContext?.state.streaming, parsingContext?.state.lastError, parsingContext?.state.lastResult, streamingReparseState],
|
||||
);
|
||||
|
||||
// Derive effective parsing state
|
||||
const effectiveParsingPhotoId = useMemo(() => {
|
||||
if (parsingContext?.state.activeAttachmentId) {
|
||||
return parsingContext.state.activeAttachmentId;
|
||||
}
|
||||
return parsingPhotoId;
|
||||
}, [parsingContext?.state.activeAttachmentId, parsingPhotoId]);
|
||||
|
||||
const effectiveReparsingPhotoId = useMemo(() => {
|
||||
if (
|
||||
parsingContext?.state.activeAttachmentId &&
|
||||
parsingContext.state.streaming?.streamType === "reparse"
|
||||
) {
|
||||
return parsingContext.state.activeAttachmentId;
|
||||
}
|
||||
return reparsingPhotoId;
|
||||
}, [
|
||||
parsingContext?.state.activeAttachmentId,
|
||||
parsingContext?.state.streaming?.streamType,
|
||||
reparsingPhotoId,
|
||||
]);
|
||||
|
||||
// Effective handlers - prefer context if available
|
||||
// Handlers using context directly
|
||||
const handleParse = useCallback(
|
||||
(
|
||||
photoId: string,
|
||||
|
|
@ -334,56 +217,36 @@ export function PhotoViewerEditor({
|
|||
{ x: number; y: number; width: number; height: number }
|
||||
>,
|
||||
) => {
|
||||
if (parsingContext) {
|
||||
parsingContext.startParse({
|
||||
attachmentId: photoId,
|
||||
modelConfigId,
|
||||
additionalContext,
|
||||
preservedBoundingBoxes,
|
||||
});
|
||||
} else if (onParse) {
|
||||
onParse(photoId, modelConfigId, additionalContext, preservedBoundingBoxes);
|
||||
}
|
||||
parsingContext.startParse({
|
||||
attachmentId: photoId,
|
||||
modelConfigId,
|
||||
additionalContext,
|
||||
preservedBoundingBoxes,
|
||||
});
|
||||
},
|
||||
[parsingContext, onParse],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
const handleCancelStreaming = useCallback(() => {
|
||||
if (parsingContext) {
|
||||
parsingContext.cancel();
|
||||
} else if (onCancelStreamingParse) {
|
||||
onCancelStreamingParse();
|
||||
}
|
||||
}, [parsingContext, onCancelStreamingParse]);
|
||||
parsingContext.cancel();
|
||||
}, [parsingContext]);
|
||||
|
||||
const handleCancelStreamingReparse = useCallback(() => {
|
||||
if (parsingContext) {
|
||||
parsingContext.cancel();
|
||||
} else if (onCancelStreamingReparse) {
|
||||
onCancelStreamingReparse();
|
||||
}
|
||||
}, [parsingContext, onCancelStreamingReparse]);
|
||||
parsingContext.cancel();
|
||||
}, [parsingContext]);
|
||||
|
||||
const handleApprove = useCallback(
|
||||
(photoId: string) => {
|
||||
if (parsingContext) {
|
||||
parsingContext.approve(photoId);
|
||||
} else if (onApprove) {
|
||||
onApprove(photoId);
|
||||
}
|
||||
parsingContext.approve(photoId);
|
||||
},
|
||||
[parsingContext, onApprove],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
const handleSubmitCorrection = useCallback(
|
||||
async (photoId: string, correction: ProblemCorrection) => {
|
||||
if (parsingContext) {
|
||||
await parsingContext.submitCorrection(photoId, [correction]);
|
||||
} else if (onSubmitCorrection) {
|
||||
await onSubmitCorrection(photoId, correction);
|
||||
}
|
||||
await parsingContext.submitCorrection(photoId, [correction]);
|
||||
},
|
||||
[parsingContext, onSubmitCorrection],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
const executeReparseSelected = useCallback(
|
||||
|
|
@ -398,23 +261,14 @@ export function PhotoViewerEditor({
|
|||
}>,
|
||||
additionalContext?: string,
|
||||
) => {
|
||||
if (parsingContext) {
|
||||
await parsingContext.startReparse({
|
||||
attachmentId: photoId,
|
||||
problemIndices,
|
||||
boundingBoxes,
|
||||
additionalContext,
|
||||
});
|
||||
} else if (onReparseSelected) {
|
||||
await onReparseSelected(
|
||||
photoId,
|
||||
problemIndices,
|
||||
boundingBoxes,
|
||||
additionalContext,
|
||||
);
|
||||
}
|
||||
await parsingContext.startReparse({
|
||||
attachmentId: photoId,
|
||||
problemIndices,
|
||||
boundingBoxes,
|
||||
additionalContext,
|
||||
});
|
||||
},
|
||||
[parsingContext, onReparseSelected],
|
||||
[parsingContext],
|
||||
);
|
||||
|
||||
// Reset state when opening
|
||||
|
|
@ -595,10 +449,7 @@ export function PhotoViewerEditor({
|
|||
|
||||
// Confirm and execute re-parse
|
||||
const confirmReparseSelected = useCallback(async () => {
|
||||
// Check if we can execute - need either context or prop handler
|
||||
const canExecute = parsingContext || onReparseSelected;
|
||||
if (!currentPhoto || !canExecute || selectedForReparse.size === 0)
|
||||
return;
|
||||
if (!currentPhoto || selectedForReparse.size === 0) return;
|
||||
|
||||
const problems = currentPhoto.rawParsingResult?.problems ?? [];
|
||||
const indices = Array.from(selectedForReparse).sort((a, b) => a - b);
|
||||
|
|
@ -629,13 +480,11 @@ export function PhotoViewerEditor({
|
|||
setAdjustedBoxes(new Map());
|
||||
|
||||
await executeReparseSelected(currentPhoto.id, indices, boundingBoxes);
|
||||
}, [currentPhoto, parsingContext, onReparseSelected, selectedForReparse, adjustedBoxes, executeReparseSelected]);
|
||||
}, [currentPhoto, selectedForReparse, adjustedBoxes, executeReparseSelected]);
|
||||
|
||||
// Bulk exclude selected problems
|
||||
const handleExcludeSelected = useCallback(async () => {
|
||||
const canSubmit = parsingContext || onSubmitCorrection;
|
||||
if (!currentPhoto || !canSubmit || selectedForReparse.size === 0)
|
||||
return;
|
||||
if (!currentPhoto || selectedForReparse.size === 0) return;
|
||||
|
||||
const problems = currentPhoto.rawParsingResult?.problems ?? [];
|
||||
const indices = Array.from(selectedForReparse).sort((a, b) => a - b);
|
||||
|
|
@ -654,13 +503,11 @@ export function PhotoViewerEditor({
|
|||
// Clear selections after excluding
|
||||
setSelectedForReparse(new Set());
|
||||
setAdjustedBoxes(new Map());
|
||||
}, [currentPhoto, parsingContext, onSubmitCorrection, selectedForReparse, handleSubmitCorrection]);
|
||||
}, [currentPhoto, selectedForReparse, handleSubmitCorrection]);
|
||||
|
||||
// Bulk restore selected problems
|
||||
const handleRestoreSelected = useCallback(async () => {
|
||||
const canSubmit = parsingContext || onSubmitCorrection;
|
||||
if (!currentPhoto || !canSubmit || selectedForReparse.size === 0)
|
||||
return;
|
||||
if (!currentPhoto || selectedForReparse.size === 0) return;
|
||||
|
||||
const problems = currentPhoto.rawParsingResult?.problems ?? [];
|
||||
const indices = Array.from(selectedForReparse).sort((a, b) => a - b);
|
||||
|
|
@ -679,7 +526,7 @@ export function PhotoViewerEditor({
|
|||
// Clear selections after restoring
|
||||
setSelectedForReparse(new Set());
|
||||
setAdjustedBoxes(new Map());
|
||||
}, [currentPhoto, parsingContext, onSubmitCorrection, selectedForReparse, handleSubmitCorrection]);
|
||||
}, [currentPhoto, selectedForReparse, handleSubmitCorrection]);
|
||||
|
||||
// Count how many selected problems are excluded vs non-excluded
|
||||
const selectedExcludedCount = useMemo(() => {
|
||||
|
|
@ -1047,16 +894,11 @@ export function PhotoViewerEditor({
|
|||
onReviewSubModeChange={setReviewSubMode}
|
||||
showReparsePreview={showReparsePreview}
|
||||
selectedForReparseCount={selectedForReparse.size}
|
||||
canParse={!!(parsingContext || onParse)}
|
||||
isParsing={effectiveParsingPhotoId === currentPhoto.id}
|
||||
isReparsing={
|
||||
effectiveReparsingPhotoId === currentPhoto.id ||
|
||||
effectiveStreamingReparseState?.status === "connecting" ||
|
||||
effectiveStreamingReparseState?.status === "processing" ||
|
||||
false
|
||||
}
|
||||
canParse={true}
|
||||
isParsing={activeAttachmentId === currentPhoto.id && isInitialParsing}
|
||||
isReparsing={activeAttachmentId === currentPhoto.id && isReparsing}
|
||||
isApproving={approvingPhotoId === currentPhoto.id}
|
||||
canApprove={!!(parsingContext || onApprove)}
|
||||
canApprove={true}
|
||||
onBack={() => {
|
||||
setMode("view");
|
||||
setSelectedProblemIndex(null);
|
||||
|
|
@ -1077,7 +919,7 @@ export function PhotoViewerEditor({
|
|||
}
|
||||
}}
|
||||
onApprove={() => {
|
||||
if ((parsingContext || onApprove) && currentPhoto) {
|
||||
if (currentPhoto) {
|
||||
handleApprove(currentPhoto.id);
|
||||
}
|
||||
}}
|
||||
|
|
@ -1115,7 +957,7 @@ export function PhotoViewerEditor({
|
|||
}
|
||||
}}
|
||||
onCorrectProblem={async (problemIndex, correction) => {
|
||||
if ((parsingContext || onSubmitCorrection) && currentPhoto) {
|
||||
if (currentPhoto) {
|
||||
await handleSubmitCorrection(currentPhoto.id, correction);
|
||||
}
|
||||
}}
|
||||
|
|
@ -1226,9 +1068,7 @@ export function PhotoViewerEditor({
|
|||
</h3>
|
||||
|
||||
{/* Streaming re-parse progress */}
|
||||
{effectiveStreamingReparseState &&
|
||||
(effectiveStreamingReparseState.status === "connecting" ||
|
||||
effectiveStreamingReparseState.status === "processing") ? (
|
||||
{isReparsing && streamingState ? (
|
||||
<div
|
||||
data-element="streaming-reparse-progress"
|
||||
className={css({
|
||||
|
|
@ -1275,33 +1115,31 @@ export function PhotoViewerEditor({
|
|||
color: "blue.300",
|
||||
})}
|
||||
>
|
||||
{effectiveStreamingReparseState.progressMessage ||
|
||||
`Problem ${effectiveStreamingReparseState.currentProblemIndex + 1} of ${effectiveStreamingReparseState.totalProblems}`}
|
||||
{streamingState.progressMessage ||
|
||||
`Problem ${(streamingState.currentProblemIndex ?? 0) + 1} of ${streamingState.totalProblems ?? 0}`}
|
||||
</div>
|
||||
</div>
|
||||
{/* Cancel button */}
|
||||
{(parsingContext || onCancelStreamingReparse) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelStreamingReparse}
|
||||
className={css({
|
||||
marginLeft: "auto",
|
||||
padding: "4px 12px",
|
||||
fontSize: "xs",
|
||||
fontWeight: "medium",
|
||||
color: "blue.300",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid token(colors.blue.500)",
|
||||
borderRadius: "md",
|
||||
cursor: "pointer",
|
||||
_hover: {
|
||||
backgroundColor: "blue.800",
|
||||
},
|
||||
})}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelStreamingReparse}
|
||||
className={css({
|
||||
marginLeft: "auto",
|
||||
padding: "4px 12px",
|
||||
fontSize: "xs",
|
||||
fontWeight: "medium",
|
||||
color: "blue.300",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid token(colors.blue.500)",
|
||||
borderRadius: "md",
|
||||
cursor: "pointer",
|
||||
_hover: {
|
||||
backgroundColor: "blue.800",
|
||||
},
|
||||
})}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
|
|
@ -1321,9 +1159,9 @@ export function PhotoViewerEditor({
|
|||
})}
|
||||
style={{
|
||||
width: `${
|
||||
effectiveStreamingReparseState.totalProblems > 0
|
||||
? (effectiveStreamingReparseState.currentProblemIndex /
|
||||
effectiveStreamingReparseState.totalProblems) *
|
||||
(streamingState.totalProblems ?? 0) > 0
|
||||
? ((streamingState.currentProblemIndex ?? 0) /
|
||||
(streamingState.totalProblems ?? 1)) *
|
||||
100
|
||||
: 0
|
||||
}%`,
|
||||
|
|
@ -1332,7 +1170,7 @@ export function PhotoViewerEditor({
|
|||
</div>
|
||||
|
||||
{/* Reasoning text panel */}
|
||||
{effectiveStreamingReparseState.reasoningText && (
|
||||
{streamingState.reasoningText && (
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
|
|
@ -1363,13 +1201,13 @@ export function PhotoViewerEditor({
|
|||
fontFamily: "monospace",
|
||||
})}
|
||||
>
|
||||
{effectiveStreamingReparseState.reasoningText}
|
||||
{streamingState.reasoningText}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completed problems list */}
|
||||
{effectiveStreamingReparseState.completedIndices.length > 0 && (
|
||||
{(streamingState.completedIndices?.length ?? 0) > 0 && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: "xs",
|
||||
|
|
@ -1377,7 +1215,7 @@ export function PhotoViewerEditor({
|
|||
})}
|
||||
>
|
||||
Completed:{" "}
|
||||
{effectiveStreamingReparseState.completedIndices
|
||||
{(streamingState.completedIndices ?? [])
|
||||
.map((i) => `#${i + 1}`)
|
||||
.join(", ")}
|
||||
</div>
|
||||
|
|
@ -1581,7 +1419,7 @@ export function PhotoViewerEditor({
|
|||
)
|
||||
}
|
||||
onSubmitCorrection={(correction) => {
|
||||
if ((parsingContext || onSubmitCorrection) && currentPhoto) {
|
||||
if (currentPhoto) {
|
||||
handleSubmitCorrection(currentPhoto.id, correction);
|
||||
}
|
||||
}}
|
||||
|
|
@ -1755,7 +1593,7 @@ export function PhotoViewerEditor({
|
|||
isOpen={showReparseModal}
|
||||
onClose={() => setShowReparseModal(false)}
|
||||
onConfirm={(hints) => {
|
||||
if ((parsingContext || onParse) && currentPhoto) {
|
||||
if (currentPhoto) {
|
||||
// Pass adjusted bounding boxes if any exist
|
||||
const preserved =
|
||||
adjustedBoxes.size > 0
|
||||
|
|
@ -1765,7 +1603,7 @@ export function PhotoViewerEditor({
|
|||
setShowReparseModal(false);
|
||||
}
|
||||
}}
|
||||
isProcessing={effectiveParsingPhotoId === currentPhoto.id}
|
||||
isProcessing={activeAttachmentId === currentPhoto.id && isInitialParsing}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1872,8 +1710,7 @@ export function PhotoViewerEditor({
|
|||
</button>
|
||||
|
||||
{/* Parse button - split button with model selection dropdown */}
|
||||
{(parsingContext || onParse) &&
|
||||
(!currentPhoto.parsingStatus ||
|
||||
{(!currentPhoto.parsingStatus ||
|
||||
currentPhoto.parsingStatus === "failed") &&
|
||||
!currentPhoto.sessionCreated && (
|
||||
<div
|
||||
|
|
@ -1894,12 +1731,7 @@ export function PhotoViewerEditor({
|
|||
: undefined;
|
||||
handleParse(currentPhoto.id, undefined, undefined, preserved);
|
||||
}}
|
||||
disabled={
|
||||
effectiveParsingPhotoId === currentPhoto.id ||
|
||||
effectiveStreamingState?.status === "connecting" ||
|
||||
effectiveStreamingState?.status === "reasoning" ||
|
||||
effectiveStreamingState?.status === "generating"
|
||||
}
|
||||
disabled={isInitialParsing}
|
||||
className={css({
|
||||
px: 4,
|
||||
py: 2,
|
||||
|
|
@ -1934,10 +1766,7 @@ export function PhotoViewerEditor({
|
|||
: "Parse worksheet"
|
||||
}
|
||||
>
|
||||
{effectiveParsingPhotoId === currentPhoto.id ||
|
||||
effectiveStreamingState?.status === "connecting" ||
|
||||
effectiveStreamingState?.status === "reasoning" ||
|
||||
effectiveStreamingState?.status === "generating" ? (
|
||||
{isInitialParsing ? (
|
||||
<>
|
||||
<span
|
||||
className={css({
|
||||
|
|
@ -1947,11 +1776,11 @@ export function PhotoViewerEditor({
|
|||
⏳
|
||||
</span>
|
||||
<span>
|
||||
{effectiveStreamingState?.status === "connecting"
|
||||
{streamingState?.status === "connecting"
|
||||
? "Connecting..."
|
||||
: effectiveStreamingState?.status === "reasoning"
|
||||
: streamingState?.status === "reasoning"
|
||||
? "Thinking..."
|
||||
: effectiveStreamingState?.status === "generating"
|
||||
: streamingState?.status === "generating"
|
||||
? "Extracting..."
|
||||
: "Analyzing..."}
|
||||
</span>
|
||||
|
|
@ -1976,12 +1805,7 @@ export function PhotoViewerEditor({
|
|||
type="button"
|
||||
data-action="toggle-model-dropdown"
|
||||
onClick={() => setIsModelDropdownOpen(!isModelDropdownOpen)}
|
||||
disabled={
|
||||
effectiveParsingPhotoId === currentPhoto.id ||
|
||||
effectiveStreamingState?.status === "connecting" ||
|
||||
effectiveStreamingState?.status === "reasoning" ||
|
||||
effectiveStreamingState?.status === "generating"
|
||||
}
|
||||
disabled={isInitialParsing}
|
||||
className={css({
|
||||
px: 2,
|
||||
py: 2,
|
||||
|
|
@ -2143,10 +1967,7 @@ export function PhotoViewerEditor({
|
|||
)}
|
||||
|
||||
{/* Streaming Parse Progress Panel */}
|
||||
{effectiveStreamingState &&
|
||||
(effectiveStreamingState.status === "connecting" ||
|
||||
effectiveStreamingState.status === "reasoning" ||
|
||||
effectiveStreamingState.status === "generating") && (
|
||||
{isInitialParsing && streamingState && (
|
||||
<div
|
||||
data-element="streaming-parse-progress"
|
||||
className={css({
|
||||
|
|
@ -2194,39 +2015,37 @@ export function PhotoViewerEditor({
|
|||
color: "blue.300",
|
||||
})}
|
||||
>
|
||||
{effectiveStreamingState.status === "connecting"
|
||||
{streamingState.status === "connecting"
|
||||
? "Connecting to AI..."
|
||||
: effectiveStreamingState.status === "reasoning"
|
||||
: streamingState.status === "reasoning"
|
||||
? "AI is analyzing worksheet..."
|
||||
: "Extracting problems..."}
|
||||
</span>
|
||||
</div>
|
||||
{(parsingContext || onCancelStreamingParse) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelStreaming}
|
||||
className={css({
|
||||
padding: "4px 12px",
|
||||
fontSize: "xs",
|
||||
fontWeight: "medium",
|
||||
color: "gray.400",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid token(colors.gray.600)",
|
||||
borderRadius: "md",
|
||||
cursor: "pointer",
|
||||
_hover: {
|
||||
backgroundColor: "gray.700",
|
||||
color: "white",
|
||||
},
|
||||
})}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelStreaming}
|
||||
className={css({
|
||||
padding: "4px 12px",
|
||||
fontSize: "xs",
|
||||
fontWeight: "medium",
|
||||
color: "gray.400",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid token(colors.gray.600)",
|
||||
borderRadius: "md",
|
||||
cursor: "pointer",
|
||||
_hover: {
|
||||
backgroundColor: "gray.700",
|
||||
color: "white",
|
||||
},
|
||||
})}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reasoning text - collapsible */}
|
||||
{effectiveStreamingState.reasoningText && (
|
||||
{streamingState.reasoningText && (
|
||||
<div
|
||||
className={css({
|
||||
maxHeight: "120px",
|
||||
|
|
@ -2240,12 +2059,12 @@ export function PhotoViewerEditor({
|
|||
fontFamily: "monospace",
|
||||
})}
|
||||
>
|
||||
{effectiveStreamingState.reasoningText}
|
||||
{streamingState.reasoningText}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress indicator for problems found */}
|
||||
{effectiveStreamingState.completedProblems.length > 0 && (
|
||||
{(streamingState.completedProblems?.length ?? 0) > 0 && (
|
||||
<div
|
||||
className={css({
|
||||
marginTop: 2,
|
||||
|
|
@ -2253,7 +2072,7 @@ export function PhotoViewerEditor({
|
|||
color: "green.400",
|
||||
})}
|
||||
>
|
||||
Found {effectiveStreamingState.completedProblems.length}{" "}
|
||||
Found {streamingState.completedProblems?.length ?? 0}{" "}
|
||||
problems...
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
type PhotoViewerEditorPhoto,
|
||||
} from "../practice/PhotoViewerEditor";
|
||||
import { VisualDebugProvider } from "@/contexts/VisualDebugContext";
|
||||
import { MockWorksheetParsingProvider } from "@/contexts/WorksheetParsingContext.mock";
|
||||
|
||||
// Create a fresh query client for stories
|
||||
function createQueryClient() {
|
||||
|
|
@ -1170,7 +1171,9 @@ function FullFlowDemo() {
|
|||
export const FullFlow: SummaryStory = {
|
||||
render: () => (
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<FullFlowDemo />
|
||||
<MockWorksheetParsingProvider>
|
||||
<FullFlowDemo />
|
||||
</MockWorksheetParsingProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
parameters: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,224 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Mock Worksheet Parsing Context Provider
|
||||
*
|
||||
* Provides a controllable mock of the WorksheetParsingContext for use in:
|
||||
* - Storybook stories (testing specific UI states)
|
||||
* - Unit tests (isolated component testing)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // In a Storybook story
|
||||
* <MockWorksheetParsingProvider
|
||||
* state={{
|
||||
* activeAttachmentId: "photo-1",
|
||||
* streaming: {
|
||||
* status: "generating",
|
||||
* streamType: "initial",
|
||||
* reasoningText: "Analyzing worksheet...",
|
||||
* // ...
|
||||
* },
|
||||
* }}
|
||||
* >
|
||||
* <OfflineWorkSection {...props} />
|
||||
* </MockWorksheetParsingProvider>
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import {
|
||||
initialParsingState,
|
||||
isParsingAttachment as isParsingAttachmentHelper,
|
||||
isAnyParsingActive as isAnyParsingActiveHelper,
|
||||
getStreamingStatus as getStreamingStatusHelper,
|
||||
type ParsingContextState,
|
||||
} from "@/lib/worksheet-parsing/state-machine";
|
||||
import type {
|
||||
WorksheetParsingContextValue,
|
||||
ApproveResponse,
|
||||
} from "./WorksheetParsingContext";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Provider
|
||||
// ============================================================================
|
||||
|
||||
interface MockWorksheetParsingProviderProps {
|
||||
children: ReactNode;
|
||||
/** Override the default initial state */
|
||||
state?: Partial<ParsingContextState>;
|
||||
/** Mock implementations for actions (optional - defaults to no-ops) */
|
||||
actions?: Partial<{
|
||||
startParse: WorksheetParsingContextValue["startParse"];
|
||||
startReparse: WorksheetParsingContextValue["startReparse"];
|
||||
cancel: WorksheetParsingContextValue["cancel"];
|
||||
reset: WorksheetParsingContextValue["reset"];
|
||||
submitCorrection: WorksheetParsingContextValue["submitCorrection"];
|
||||
approve: WorksheetParsingContextValue["approve"];
|
||||
unapprove: WorksheetParsingContextValue["unapprove"];
|
||||
}>;
|
||||
}
|
||||
|
||||
// Re-use the same context from the real provider
|
||||
const MockWorksheetParsingContext =
|
||||
createContext<WorksheetParsingContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Mock provider for testing and Storybook
|
||||
*
|
||||
* Allows explicit control over parsing state without real API calls.
|
||||
*/
|
||||
export function MockWorksheetParsingProvider({
|
||||
children,
|
||||
state: stateOverrides = {},
|
||||
actions = {},
|
||||
}: MockWorksheetParsingProviderProps) {
|
||||
// Merge overrides with initial state
|
||||
const state: ParsingContextState = {
|
||||
...initialParsingState,
|
||||
...stateOverrides,
|
||||
};
|
||||
|
||||
// Create mock context value with no-op defaults for actions
|
||||
const value: WorksheetParsingContextValue = {
|
||||
state,
|
||||
|
||||
// Derived helpers use actual logic with mock state
|
||||
isParsingAttachment: (attachmentId: string) =>
|
||||
isParsingAttachmentHelper(state, attachmentId),
|
||||
isAnyParsingActive: () => isAnyParsingActiveHelper(state),
|
||||
getStreamingStatus: (attachmentId: string) =>
|
||||
getStreamingStatusHelper(state, attachmentId),
|
||||
|
||||
// Actions default to no-ops but can be overridden
|
||||
startParse: actions.startParse ?? (async () => {}),
|
||||
startReparse: actions.startReparse ?? (async () => {}),
|
||||
cancel: actions.cancel ?? (() => {}),
|
||||
reset: actions.reset ?? (() => {}),
|
||||
submitCorrection: actions.submitCorrection ?? (async () => {}),
|
||||
approve:
|
||||
actions.approve ??
|
||||
(async (): Promise<ApproveResponse> => ({
|
||||
success: true,
|
||||
sessionId: "mock-session-id",
|
||||
problemCount: 0,
|
||||
correctCount: 0,
|
||||
accuracy: null,
|
||||
skillsExercised: [],
|
||||
stats: {
|
||||
totalProblems: 0,
|
||||
correctCount: 0,
|
||||
incorrectCount: 0,
|
||||
unansweredCount: 0,
|
||||
accuracy: null,
|
||||
skillsDetected: [],
|
||||
},
|
||||
})),
|
||||
unapprove: actions.unapprove ?? (async () => {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<MockWorksheetParsingContext.Provider value={value}>
|
||||
{children}
|
||||
</MockWorksheetParsingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the mock context
|
||||
*
|
||||
* Can be used interchangeably with useWorksheetParsingContext in tests
|
||||
*/
|
||||
export function useMockWorksheetParsingContext(): WorksheetParsingContextValue {
|
||||
const context = useContext(MockWorksheetParsingContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useMockWorksheetParsingContext must be used within a MockWorksheetParsingProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Storybook Helpers
|
||||
// ============================================================================
|
||||
|
||||
/** Pre-configured states for common Storybook scenarios */
|
||||
export const mockParsingStates = {
|
||||
/** Initial idle state - no parsing activity */
|
||||
idle: initialParsingState,
|
||||
|
||||
/** Actively parsing a worksheet */
|
||||
parsing: (attachmentId: string): Partial<ParsingContextState> => ({
|
||||
activeAttachmentId: attachmentId,
|
||||
streaming: {
|
||||
status: "generating",
|
||||
streamType: "initial",
|
||||
reasoningText: "I can see a worksheet with arithmetic problems...",
|
||||
outputText: '{"problems": [',
|
||||
progressMessage: "Extracting problems... 5 found",
|
||||
completedProblems: [],
|
||||
},
|
||||
}),
|
||||
|
||||
/** Parsing complete with results */
|
||||
complete: (attachmentId: string): Partial<ParsingContextState> => ({
|
||||
activeAttachmentId: null,
|
||||
streaming: null,
|
||||
lastResult: {
|
||||
problems: [],
|
||||
pageMetadata: {
|
||||
lessonId: null,
|
||||
weekId: null,
|
||||
pageNumber: null,
|
||||
detectedFormat: "vertical",
|
||||
totalRows: 0,
|
||||
problemsPerRow: 0,
|
||||
},
|
||||
overallConfidence: 0.95,
|
||||
needsReview: false,
|
||||
warnings: [],
|
||||
},
|
||||
lastStats: {
|
||||
totalProblems: 10,
|
||||
correctCount: 8,
|
||||
incorrectCount: 2,
|
||||
unansweredCount: 0,
|
||||
accuracy: 0.8,
|
||||
skillsDetected: ["add-1-digit"],
|
||||
},
|
||||
lastError: null,
|
||||
}),
|
||||
|
||||
/** Parsing failed with error */
|
||||
error: (
|
||||
attachmentId: string,
|
||||
errorMessage: string,
|
||||
): Partial<ParsingContextState> => ({
|
||||
activeAttachmentId: null,
|
||||
streaming: null,
|
||||
lastResult: null,
|
||||
lastStats: null,
|
||||
lastError: errorMessage,
|
||||
}),
|
||||
|
||||
/** Re-parsing specific problems */
|
||||
reparsing: (
|
||||
attachmentId: string,
|
||||
currentIndex: number,
|
||||
total: number,
|
||||
): Partial<ParsingContextState> => ({
|
||||
activeAttachmentId: attachmentId,
|
||||
streaming: {
|
||||
status: "processing",
|
||||
streamType: "reparse",
|
||||
reasoningText: "Re-analyzing this problem more carefully...",
|
||||
outputText: "",
|
||||
progressMessage: `Re-parsing problem ${currentIndex + 1} of ${total}`,
|
||||
completedProblems: [],
|
||||
currentProblemIndex: currentIndex,
|
||||
totalProblems: total,
|
||||
completedIndices: Array.from({ length: currentIndex }, (_, i) => i),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
@ -92,7 +92,7 @@ export interface StartReparseOptions {
|
|||
}
|
||||
|
||||
/** Response from approve API */
|
||||
interface ApproveResponse {
|
||||
export interface ApproveResponse {
|
||||
success: boolean;
|
||||
sessionId: string;
|
||||
problemCount: number;
|
||||
|
|
|
|||
|
|
@ -229,70 +229,12 @@ export function useStartParsing(playerId: string, sessionId: string) {
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
// Streaming Parsing Hook
|
||||
// Streaming State Management
|
||||
// ============================================================================
|
||||
|
||||
/** Streaming parsing state */
|
||||
export interface StreamingParseState {
|
||||
/** Current status */
|
||||
status:
|
||||
| "idle"
|
||||
| "connecting"
|
||||
| "reasoning"
|
||||
| "generating"
|
||||
| "complete"
|
||||
| "error";
|
||||
/** Accumulated reasoning text (model's thinking process) */
|
||||
reasoningText: string;
|
||||
/** Accumulated output text (partial JSON) */
|
||||
outputText: string;
|
||||
/** Error message if failed */
|
||||
error: string | null;
|
||||
/** Progress stage message */
|
||||
progressMessage: string | null;
|
||||
/** Final result when complete */
|
||||
result: WorksheetParsingResult | null;
|
||||
/** Final stats when complete */
|
||||
stats: ParsingStats | null;
|
||||
/** Problems that have been fully streamed (for progressive highlighting) */
|
||||
completedProblems: CompletedProblem[];
|
||||
}
|
||||
|
||||
// NOTE: useStreamingParse hook was removed - use WorksheetParsingContext instead
|
||||
|
||||
// ============================================================================
|
||||
// Streaming Re-parse State Types
|
||||
// ============================================================================
|
||||
|
||||
/** Streaming re-parse state */
|
||||
export interface StreamingReparseState {
|
||||
/** Current status */
|
||||
status:
|
||||
| "idle"
|
||||
| "connecting"
|
||||
| "processing"
|
||||
| "complete"
|
||||
| "error"
|
||||
| "cancelled";
|
||||
/** Current problem being processed (index in problemIndices array) */
|
||||
currentProblemIndex: number;
|
||||
/** Total problems to process */
|
||||
totalProblems: number;
|
||||
/** Current problem's worksheet index */
|
||||
currentProblemWorksheetIndex: number | null;
|
||||
/** Accumulated reasoning text for current problem */
|
||||
reasoningText: string;
|
||||
/** Error message if failed */
|
||||
error: string | null;
|
||||
/** Progress message */
|
||||
progressMessage: string | null;
|
||||
/** Completed problem indices */
|
||||
completedIndices: number[];
|
||||
/** Final result when complete */
|
||||
result: WorksheetParsingResult | null;
|
||||
}
|
||||
|
||||
// NOTE: useStreamingReparse hook was removed - use WorksheetParsingContext instead
|
||||
// NOTE: Streaming state is now managed via WorksheetParsingContext.
|
||||
// See src/contexts/WorksheetParsingContext.tsx for the context provider
|
||||
// and src/lib/worksheet-parsing/state-machine.ts for the state types.
|
||||
|
||||
/**
|
||||
* Hook to submit corrections to parsed problems
|
||||
|
|
|
|||
Loading…
Reference in New Issue