Spaces:
Sleeping
Sleeping
| "use client"; | |
| import * as React from "react"; | |
| import { useForm, useFieldArray } from "react-hook-form"; | |
| import { zodResolver } from "@hookform/resolvers/zod"; | |
| import { LagrangeFormSchema, type LagrangeFormValues, type DataPoint } from "@/lib/schemas"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; | |
| import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; | |
| import { PlusCircle, MinusCircle, Calculator, Sigma, Target, AlertTriangle, ListChecks, FunctionSquare, Spline } from "lucide-react"; | |
| import { calculateLagrangeInterpolation, type CalculationStep } from "@/app/actions"; | |
| import { useToast } from "@/hooks/use-toast"; | |
| import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; | |
| import { | |
| ChartContainer, | |
| ChartTooltip, | |
| ChartTooltipContent, | |
| ChartLegend, | |
| ChartLegendContent, | |
| } from "@/components/ui/chart"; | |
| import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend as RechartsLegend, Scatter, ReferenceDot, Label as RechartsLabel, ReferenceLine } from "recharts"; | |
| import { cn } from "@/lib/utils"; | |
| // Helper component to render fractions | |
| const SymbolicFraction: React.FC<{ numerator: string | React.ReactNode; denominator: string | React.ReactNode; className?: string }> = ({ numerator, denominator, className }) => { | |
| return ( | |
| <div className={cn("inline-flex flex-col items-center text-center align-middle mx-1 leading-tight", className)}> | |
| <span className="px-1 pb-0.5">{numerator}</span> | |
| <hr className="w-full border-foreground my-0.5" /> | |
| <span className="px-1 pt-0.5">{denominator}</span> | |
| </div> | |
| ); | |
| }; | |
| export function LagrangeSolver() { | |
| const { toast } = useToast(); | |
| const [isLoading, setIsLoading] = React.useState(false); | |
| const [result, setResult] = React.useState<{ | |
| interpolatedValue: number | null; | |
| polynomialTermsDisplay: string[]; | |
| calculationSteps: CalculationStep[]; | |
| plotData?: Array<{ x: number; original?: number; interpolated?: number; target?: number }>; | |
| interpolationPoint?: number; | |
| } | null>(null); | |
| const [error, setError] = React.useState<string | null>(null); | |
| const form = useForm<LagrangeFormValues>({ | |
| resolver: zodResolver(LagrangeFormSchema), | |
| defaultValues: { | |
| dataPoints: [{ x: "", y: "" }, { x: "", y: "" }], | |
| interpolationX: "", | |
| }, | |
| }); | |
| const { fields, append, remove } = useFieldArray({ | |
| control: form.control, | |
| name: "dataPoints", | |
| }); | |
| const onSubmit = async (values: LagrangeFormValues) => { | |
| setIsLoading(true); | |
| setResult(null); | |
| setError(null); | |
| const numericDataPoints: DataPoint[] = values.dataPoints.map(dp => ({ | |
| x: parseFloat(dp.x), | |
| y: parseFloat(dp.y), | |
| })); | |
| const numericInterpolationX = parseFloat(values.interpolationX); | |
| try { | |
| const response = await calculateLagrangeInterpolation(numericDataPoints, numericInterpolationX); | |
| if (response.error) { | |
| setError(response.error); | |
| toast({ | |
| title: "Calculation Error", | |
| description: response.error, | |
| variant: "destructive", | |
| }); | |
| } else { | |
| setResult(response); | |
| } | |
| } catch (e) { | |
| const errorMessage = e instanceof Error ? e.message : "An unknown error occurred."; | |
| setError(errorMessage); | |
| toast({ | |
| title: "Unhandled Error", | |
| description: errorMessage, | |
| variant: "destructive", | |
| }); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const chartConfig = { | |
| original: { label: "Original Data", color: "hsl(var(--primary))" }, | |
| interpolated: { label: "Lagrange Polynomial", color: "hsl(var(--accent))" }, | |
| target: { label: `Interpolated L(${result?.interpolationPoint})`, color: "hsl(var(--destructive))" }, | |
| }; | |
| return ( | |
| <Form {...form}> | |
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 font-headline"> | |
| <ListChecks className="h-6 w-6 text-primary" /> | |
| Data Points (x, y) | |
| </CardTitle> | |
| <CardDescription>Enter the known data points. At least one point is required. X values must be unique.</CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| {fields.map((field, index) => ( | |
| <div key={field.id} className="flex items-start gap-4"> | |
| <FormField | |
| control={form.control} | |
| name={`dataPoints.${index}.x`} | |
| render={({ field }) => ( | |
| <FormItem className="flex-1"> | |
| <FormLabel>X<sub>{index}</sub></FormLabel> | |
| <FormControl> | |
| <Input type="text" placeholder={`X value for point ${index + 1}`} {...field} /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| <FormField | |
| control={form.control} | |
| name={`dataPoints.${index}.y`} | |
| render={({ field }) => ( | |
| <FormItem className="flex-1"> | |
| <FormLabel>Y<sub>{index}</sub></FormLabel> | |
| <FormControl> | |
| <Input type="text" placeholder={`Y value for point ${index + 1}`} {...field} /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {fields.length > 1 && ( | |
| <Button | |
| type="button" | |
| variant="ghost" | |
| size="icon" | |
| onClick={() => remove(index)} | |
| className="mt-8 text-destructive hover:text-destructive hover:bg-destructive/10" | |
| aria-label="Remove point" | |
| > | |
| <MinusCircle className="h-5 w-5" /> | |
| </Button> | |
| )} | |
| </div> | |
| ))} | |
| {form.formState.errors.dataPoints && typeof form.formState.errors.dataPoints.message === 'string' && ( | |
| <p className="text-sm font-medium text-destructive">{form.formState.errors.dataPoints.message}</p> | |
| )} | |
| {form.formState.errors.dataPoints?.root?.message && ( | |
| <p className="text-sm font-medium text-destructive">{form.formState.errors.dataPoints.root.message}</p> | |
| )} | |
| </CardContent> | |
| <CardFooter> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={() => append({ x: "", y: "" })} | |
| className="text-primary border-primary hover:bg-primary/10 hover:text-primary" | |
| > | |
| <PlusCircle className="mr-2 h-4 w-4" /> | |
| Add Point | |
| </Button> | |
| </CardFooter> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 font-headline"> | |
| <Target className="h-6 w-6 text-primary" /> | |
| Interpolation Point (X) | |
| </CardTitle> | |
| <CardDescription>Specify the X value at which to estimate the function.</CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <FormField | |
| control={form.control} | |
| name="interpolationX" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>X value for interpolation</FormLabel> | |
| <FormControl> | |
| <Input type="text" placeholder="Enter X value" {...field} /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| </CardContent> | |
| </Card> | |
| <Button type="submit" className="w-full" disabled={isLoading}> | |
| <Calculator className="mr-2 h-4 w-4" /> | |
| {isLoading ? "Calculating..." : "Calculate Interpolated Value"} | |
| </Button> | |
| {isLoading && ( | |
| <div className="text-center p-4 text-muted-foreground"> | |
| <p>Calculating, please wait...</p> | |
| </div> | |
| )} | |
| {error && !isLoading && ( | |
| <Alert variant="destructive"> | |
| <AlertTriangle className="h-4 w-4" /> | |
| <AlertTitle>Error</AlertTitle> | |
| <AlertDescription>{error}</AlertDescription> | |
| </Alert> | |
| )} | |
| {result && !isLoading && !error && ( | |
| <> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 font-headline"> | |
| <FunctionSquare className="h-6 w-6 text-primary" /> | |
| Results Summary | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div> | |
| <h3 className="font-semibold">Interpolated Value:</h3> | |
| <p className="text-2xl"> | |
| L({result.interpolationPoint?.toString()}) = | |
| <span className="font-bold text-accent">{result.interpolatedValue?.toFixed(6)}</span> | |
| </p> | |
| </div> | |
| <div> | |
| <h3 className="font-semibold">Lagrange Polynomial L(x):</h3> | |
| <p className="text-sm text-muted-foreground"> | |
| The polynomial is formed by summing these terms: | |
| </p> | |
| <div className="mt-2 p-3 bg-muted rounded-md overflow-x-auto"> | |
| <pre className="text-sm"><code className="font-code"> | |
| {result.polynomialTermsDisplay.map((term, index) => ( | |
| <React.Fragment key={index}> | |
| {term} | |
| {index < result.polynomialTermsDisplay.length - 1 ? " + " : ""} | |
| </React.Fragment> | |
| ))} | |
| </code></pre> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {result.calculationSteps && result.calculationSteps.length > 0 && ( | |
| <Card className="mt-6"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 font-headline"> | |
| <Sigma className="h-6 w-6 text-primary" /> | |
| Step-by-Step Calculation | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <Accordion type="single" collapsible className="w-full"> | |
| {result.calculationSteps.map((step, index) => ( | |
| <AccordionItem value={`item-${index}`} key={index}> | |
| <AccordionTrigger> | |
| Term {index + 1}: Contribution of y<sub>{step.termIndex}</sub> = {step.yValue.toFixed(4)} | |
| </AccordionTrigger> | |
| <AccordionContent className="space-y-4 text-sm p-4 bg-background rounded-md border"> | |
| {/* Section 1: Basis Polynomial Definition */} | |
| <div> | |
| <p className="font-semibold">Basis Polynomial L<sub>{step.termIndex}</sub>(x)</p> | |
| <p className="text-muted-foreground text-xs mt-0.5"> | |
| General form: L<sub>{step.termIndex}</sub>(x) = ∏<sub>k≠{step.termIndex}</sub> | |
| <span className="inline-block align-middle text-center mx-1"> | |
| (x - x<sub>k</sub>) / (x<sub>{step.termIndex}</sub> - x<sub>k</sub>) | |
| </span> | |
| </p> | |
| <div className="mt-2 flex items-center flex-wrap"> | |
| Actual L<sub>{step.termIndex}</sub>(x) = | |
| <SymbolicFraction | |
| numerator={step.basisNumeratorSymbolic || "1"} | |
| denominator={`(${step.basisDenominatorSymbolic || "1"})`} | |
| /> | |
| </div> | |
| <div className="mt-2 flex items-center flex-wrap"> | |
| = | |
| <SymbolicFraction | |
| numerator={step.basisNumeratorSymbolic || "1"} | |
| denominator={step.basisDenominatorValue.toFixed(6)} | |
| /> | |
| </div> | |
| </div> | |
| {/* Section 2: Evaluation */} | |
| <div> | |
| <p className="font-semibold mt-3">Evaluating L<sub>{step.termIndex}</sub>(x) at x = {result.interpolationPoint?.toString()}</p> | |
| <div className="mt-2 flex items-center flex-wrap"> | |
| L<sub>{step.termIndex}</sub>({result.interpolationPoint?.toString()}) = | |
| <SymbolicFraction | |
| numerator={step.basisNumeratorAtXValues || "1"} | |
| denominator={step.basisDenominatorValue.toFixed(6)} | |
| /> | |
| </div> | |
| <div className="mt-2 flex items-center flex-wrap"> | |
| = | |
| <SymbolicFraction | |
| numerator={step.basisNumeratorAtXProduct.toFixed(6)} | |
| denominator={step.basisDenominatorValue.toFixed(6)} | |
| /> | |
| = <span className="font-bold">{step.basisPolynomialValueAtX.toFixed(6)}</span> | |
| </div> | |
| </div> | |
| {/* Section 3: Term Contribution */} | |
| <div> | |
| <p className="font-semibold mt-3">Term Contribution: y<sub>{step.termIndex}</sub> · L<sub>{step.termIndex}</sub>({result.interpolationPoint?.toString()})</p> | |
| { form.getValues("dataPoints").length > 1 && (step.basisNumeratorSymbolic !== "1" || step.basisDenominatorSymbolic !== "1" || step.basisDenominatorValue !== 1.0) && | |
| <div className="mt-2 flex items-center flex-wrap"> | |
| Symbolic Term<sub>{step.termIndex}</sub> = | |
| <SymbolicFraction | |
| numerator={ | |
| <span className="whitespace-nowrap">{step.yValue.toFixed(4)} · ({step.basisNumeratorSymbolic || "1"})</span> | |
| } | |
| denominator={step.basisDenominatorValue.toFixed(6)} | |
| /> | |
| </div> | |
| } | |
| { (form.getValues("dataPoints").length === 1 || (step.basisNumeratorSymbolic === "1" && step.basisDenominatorSymbolic ==="1" && step.basisDenominatorValue === 1.0)) && | |
| <p className="mt-1"> | |
| Symbolic Term<sub>{step.termIndex}</sub> = {step.yValue.toFixed(4)} | |
| </p> | |
| } | |
| <p className="mt-2"> | |
| Numerical Term<sub>{step.termIndex}</sub> = {step.yValue.toFixed(4)} · {step.basisPolynomialValueAtX.toFixed(6)} | |
| </p> | |
| <p className="mt-1"> | |
| = <span className="font-bold text-primary">{step.termValueAtX.toFixed(6)}</span> | |
| </p> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| ))} | |
| </Accordion> | |
| <div className="mt-6 p-4 bg-muted rounded-md"> | |
| <h3 className="font-semibold text-md">Sum of Term Contributions (Final Interpolated Value):</h3> | |
| <p className="break-all text-sm mt-1"> {/* Removed font-code */} | |
| L({result.interpolationPoint?.toString()}) = {result.calculationSteps.map(s => s.termValueAtX.toFixed(6)).join(" + ")} = <span className="font-bold text-accent text-lg">{result.interpolatedValue?.toFixed(6)}</span> | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {result.plotData && result.plotData.length > 0 && ( | |
| <Card className="mt-6"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 font-headline"> | |
| <Spline className="h-6 w-6 text-primary" /> | |
| Interpolation Plot | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="h-[450px] w-full pt-6"> | |
| <ChartContainer | |
| config={chartConfig} | |
| className="h-full w-full" | |
| > | |
| <RechartsLineChart | |
| data={result.plotData} | |
| margin={{ top: 20, right: 30, left: 0, bottom: 20 }} | |
| > | |
| <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" /> | |
| <XAxis | |
| type="number" | |
| dataKey="x" | |
| name="X" | |
| allowDuplicatedCategory={false} | |
| domain={['dataMin - 1', 'dataMax + 1']} | |
| tickFormatter={(tick) => typeof tick === 'number' ? tick.toFixed(2) : tick} | |
| stroke="hsl(var(--muted-foreground))" | |
| /> | |
| <YAxis | |
| type="number" | |
| name="Y" | |
| domain={['auto', 'auto']} | |
| tickFormatter={(tick) => typeof tick === 'number' ? tick.toFixed(2) : tick} | |
| stroke="hsl(var(--muted-foreground))" | |
| /> | |
| <RechartsTooltip | |
| content={<ChartTooltipContent | |
| hideLabel | |
| formatter={(value, name, props) => { | |
| if (props.payload?.target !== undefined && name === 'target') return [`L(${props.payload.x.toFixed(2)}) = ${props.payload.target.toFixed(4)}`, null]; | |
| if (name === 'original') return [value.toFixed(4), 'Original Point']; | |
| if (name === 'interpolated') return [value.toFixed(4), 'Polynomial P(x)']; | |
| return [value, name]; | |
| }} | |
| />} | |
| cursor={{ stroke: "hsl(var(--accent))", strokeDasharray: '3 3' }} | |
| wrapperStyle={{ outline: 'none', border: '1px solid hsl(var(--border))', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-md)'}} | |
| /> | |
| <RechartsLegend content={<ChartLegendContent />} wrapperStyle={{paddingTop: '20px'}} /> | |
| <Line | |
| type="monotone" | |
| dataKey="interpolated" | |
| stroke="var(--color-interpolated)" | |
| strokeWidth={2.5} | |
| dot={false} | |
| name={chartConfig.interpolated.label} | |
| animationDuration={500} | |
| /> | |
| <Scatter | |
| dataKey="original" | |
| fill="var(--color-original)" | |
| name={chartConfig.original.label} | |
| shape="circle" | |
| r={5} | |
| /> | |
| {result.interpolationPoint !== undefined && result.interpolatedValue !== null && ( | |
| <ReferenceDot | |
| x={result.interpolationPoint} | |
| y={result.interpolatedValue} | |
| r={7} | |
| fill="var(--color-target)" | |
| stroke="hsl(var(--background))" | |
| strokeWidth={2} | |
| ifOverflow="extendDomain" | |
| isFront={true} | |
| > | |
| <RechartsLabel | |
| value={`L(${result.interpolationPoint.toFixed(2)}) = ${result.interpolatedValue.toFixed(4)}`} | |
| position="top" | |
| offset={10} | |
| fill="var(--color-target)" | |
| fontSize="0.8rem" | |
| fontWeight="bold" | |
| /> | |
| </ReferenceDot> | |
| )} | |
| {result.interpolationPoint !== undefined && result.interpolatedValue !== null && ( | |
| <> | |
| <ReferenceLine x={result.interpolationPoint} stroke="var(--color-target)" strokeDasharray="4 4" strokeOpacity={0.7}> | |
| <RechartsLabel value={`x=${result.interpolationPoint.toFixed(2)}`} position="insideTopRight" fill="var(--color-target)" fontSize="0.75rem"/> | |
| </ReferenceLine> | |
| <ReferenceLine y={result.interpolatedValue} stroke="var(--color-target)" strokeDasharray="4 4" strokeOpacity={0.7}> | |
| <RechartsLabel value={`y=${result.interpolatedValue.toFixed(2)}`} position="insideTopRight" fill="var(--color-target)" fontSize="0.75rem"/> | |
| </ReferenceLine> | |
| </> | |
| )} | |
| </RechartsLineChart> | |
| </ChartContainer> | |
| </CardContent> | |
| </Card> | |
| )} | |
| </> | |
| )} | |
| </form> | |
| </Form> | |
| ); | |
| } | |