diff --git a/builder/src/env.ts b/builder/src/env.ts index 10734159b..db15a26e3 100644 --- a/builder/src/env.ts +++ b/builder/src/env.ts @@ -30,3 +30,15 @@ export function setRecipeEnvironment(env: RecipeEnvironment) { export function getRecipeEnvironment(): RecipeEnvironment { return globalEnv; } + +// until we thread the trace IDs through the entire workflow/recipe/... + +let lastTraceSpanID: string | undefined; + +export function setLastTraceSpanID(spanID: string) { + lastTraceSpanID = spanID; +} + +export function getLastTraceSpanID(): string | undefined { + return lastTraceSpanID; +} diff --git a/builder/src/index.ts b/builder/src/index.ts index 0604ce71a..df14301d6 100644 --- a/builder/src/index.ts +++ b/builder/src/index.ts @@ -10,8 +10,10 @@ export { render, } from "./module.ts"; export { + getLastTraceSpanID, getRecipeEnvironment, type RecipeEnvironment, + setLastTraceSpanID, setRecipeEnvironment, } from "./env.ts"; export { diff --git a/charm/src/iterate.ts b/charm/src/iterate.ts index b93248ccf..f3c594f6f 100644 --- a/charm/src/iterate.ts +++ b/charm/src/iterate.ts @@ -29,7 +29,7 @@ export const genSrc = async ({ schema, steps, model, - generationId + generationId, }: { src?: string; spec?: string; @@ -88,7 +88,7 @@ export async function iterate( schema: iframe.argumentSchema, steps: plan?.steps, model, - generationId + generationId, }); return generateNewRecipeVersion(charmManager, charm, newIFrameSrc, newSpec); @@ -337,7 +337,7 @@ async function twoPhaseCodeGeneration( newSpec, schema, steps: form.plan?.steps, - generationId: form.meta.generationId + generationId: form.meta.generationId, }); const name = extractTitle(newIFrameSrc, title); // Use the generated title as fallback const newRecipeSrc = buildFullRecipe({ diff --git a/jumble/src/components/ActionBar.tsx b/jumble/src/components/ActionBar.tsx index 3dc870bc3..b8efb56e8 100644 --- a/jumble/src/components/ActionBar.tsx +++ b/jumble/src/components/ActionBar.tsx @@ -39,7 +39,7 @@ export function ActionBar() { @@ -52,7 +52,7 @@ export function ActionBar() { return ( diff --git a/jumble/src/components/FeedbackActions.tsx b/jumble/src/components/FeedbackActions.tsx new file mode 100644 index 000000000..20645eceb --- /dev/null +++ b/jumble/src/components/FeedbackActions.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from "react"; +import { FeedbackDialog } from "@/components/FeedbackDialog.tsx"; +import { submitFeedback } from "@/services/feedback.ts"; +import { getLastTraceSpanID } from "@commontools/builder"; +import { MdThumbDownOffAlt, MdThumbUpOffAlt } from "react-icons/md"; + +export function FeedbackActions() { + // States for the feedback dialog + const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [initialScore, setInitialScore] = useState(1); + const [showFeedbackButtons, setShowFeedbackButtons] = useState(false); + + // Listen for toggle-feedback event + useEffect(() => { + const handleToggleFeedback = () => { + setShowFeedbackButtons((prev) => !prev); + }; + + globalThis.addEventListener("toggle-feedback", handleToggleFeedback); + return () => { + globalThis.removeEventListener("toggle-feedback", handleToggleFeedback); + }; + }, []); + + // Handler functions for feedback actions + const handleOpenFeedback = (score: number) => { + console.log( + `Opening ${score === 1 ? "positive" : "negative"} feedback dialog`, + ); + setInitialScore(score); + setIsFeedbackDialogOpen(true); + setShowFeedbackButtons(false); + }; + + const handleCloseFeedback = () => { + setIsFeedbackDialogOpen(false); + }; + + const handleSubmitFeedback = async ( + data: { score: number; explanation: string; userInfo?: any }, + ) => { + console.log("Submitting feedback:", data); + setIsSubmitting(true); + + try { + await submitFeedback( + { + score: data.score, + explanation: data.explanation, + spanId: getLastTraceSpanID() as string, + }, + data.userInfo, + ); + + setIsFeedbackDialogOpen(false); + alert("Feedback submitted successfully! Thank you for your input."); + } catch (error) { + alert( + `Error submitting feedback: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } finally { + setIsSubmitting(false); + } + }; + + // Render feedback dialog and the popup buttons + return ( + <> + {/* Feedback dialog for submissions */} + + + {/* Popup buttons that appear when feedback button is clicked */} + {showFeedbackButtons && ( +
+ {/* This spacer places the thumbs buttons just above the feedback button */} +
+ + {/* Thumbs down button */} +
+ +
+ + {/* Thumbs up button */} +
+ +
+
+ )} + + ); +} diff --git a/jumble/src/components/FeedbackDialog.tsx b/jumble/src/components/FeedbackDialog.tsx new file mode 100644 index 000000000..8762ed383 --- /dev/null +++ b/jumble/src/components/FeedbackDialog.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { fetchUserInfo } from "@/services/feedback.ts"; + +interface FeedbackDialogProps { + isOpen: boolean; + onClose: () => void; + onSubmit: ( + data: { score: number; explanation: string; userInfo?: any }, + ) => Promise; + initialScore: number; + isSubmitting?: boolean; +} + +export function FeedbackDialog({ + isOpen, + onClose, + onSubmit, + initialScore, + isSubmitting = false, +}: FeedbackDialogProps) { + const [explanation, setExplanation] = useState(""); + + // Reset explanation when dialog opens with a new score + useEffect(() => { + if (isOpen) { + setExplanation(""); + } + }, [isOpen, initialScore]); + + // Log when dialog open state changes + useEffect(() => { + console.log( + "FeedbackDialog isOpen:", + isOpen, + "initialScore:", + initialScore, + ); + }, [isOpen, initialScore]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isOpen) return; + + if (e.key === "Escape") { + onClose(); + } else if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + e.preventDefault(); + if (!isSubmitting && explanation.trim()) { + submitFeedback(); + } + } + }; + + globalThis.addEventListener("keydown", handleKeyDown); + return () => globalThis.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose, onSubmit, explanation, isSubmitting, initialScore]); + + if (!isOpen) return null; + + const submitFeedback = async () => { + if (!explanation.trim()) return; + + console.log("Submitting feedback form with explanation:", explanation); + + // Fetch user info only when submitting + try { + const userInfo = await fetchUserInfo(); + console.log("Fetched user info before submission:", userInfo); + + // Submit feedback with the user info + await onSubmit({ + score: initialScore, + explanation, + userInfo, + }); + } catch (error) { + console.error("Error fetching user info during submission:", error); + // Continue with submission even if user info fetch fails + await onSubmit({ score: initialScore, explanation }); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + submitFeedback(); + }; + + return ( +
+
+

+ {initialScore === 1 ? "Positive Feedback" : "Improvement Feedback"} +

+
+
+ +