Skip to content

Llm feedback #1036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 14, 2025
12 changes: 12 additions & 0 deletions builder/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions builder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export {
render,
} from "./module.ts";
export {
getLastTraceSpanID,
getRecipeEnvironment,
type RecipeEnvironment,
setLastTraceSpanID,
setRecipeEnvironment,
} from "./env.ts";
export {
Expand Down
6 changes: 3 additions & 3 deletions charm/src/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const genSrc = async ({
schema,
steps,
model,
generationId
generationId,
}: {
src?: string;
spec?: string;
Expand Down Expand Up @@ -88,7 +88,7 @@ export async function iterate(
schema: iframe.argumentSchema,
steps: plan?.steps,
model,
generationId
generationId,
});

return generateNewRecipeVersion(charmManager, charm, newIFrameSrc, newSpec);
Expand Down Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions jumble/src/components/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function ActionBar() {
<NavLink
key={action.id}
to={action.to}
className={actionButtonStyles}
className={`${actionButtonStyles} ${action.className || ""}`}
style={actionButtonInlineStyles}
>
<ActionButton label={action.label}>
Expand All @@ -52,7 +52,7 @@ export function ActionBar() {
return (
<animated.button
key={action.id}
className={actionButtonStyles}
className={`${actionButtonStyles} ${action.className || ""}`}
style={actionButtonInlineStyles}
onClick={action.onClick}
>
Expand Down
118 changes: 118 additions & 0 deletions jumble/src/components/FeedbackActions.tsx
Original file line number Diff line number Diff line change
@@ -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<number>(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 */}
<FeedbackDialog
isOpen={isFeedbackDialogOpen}
onClose={handleCloseFeedback}
onSubmit={handleSubmitFeedback}
initialScore={initialScore}
isSubmitting={isSubmitting}
/>

{/* Popup buttons that appear when feedback button is clicked */}
{showFeedbackButtons && (
<div className="fixed z-[100] bottom-2 right-2 flex flex-col-reverse gap-2 pointer-events-none">
{/* This spacer places the thumbs buttons just above the feedback button */}
<div className="h-12"></div>

{/* Thumbs down button */}
<div className="pointer-events-auto">
<button
type="button"
className="w-12 h-12 cursor-pointer flex items-center justify-center border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)] bg-red-50 hover:translate-y-[-2px] relative group"
onClick={() => handleOpenFeedback(0)}
>
<MdThumbDownOffAlt className="w-6 h-6" />
<div className="absolute left-[-100px] top-1/2 -translate-y-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity z-[200] whitespace-nowrap">
Not Helpful
</div>
</button>
</div>

{/* Thumbs up button */}
<div className="pointer-events-auto">
<button
type="button"
className="w-12 h-12 cursor-pointer flex items-center justify-center border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)] bg-green-50 hover:translate-y-[-2px] relative group"
onClick={() => handleOpenFeedback(1)}
>
<MdThumbUpOffAlt className="w-6 h-6" />
<div className="absolute left-[-100px] top-1/2 -translate-y-1/2 bg-gray-800 text-white px-2 py-1 rounded text-sm opacity-0 group-hover:opacity-100 transition-opacity z-[200] whitespace-nowrap">
Helpful
</div>
</button>
</div>
</div>
)}
</>
);
}
139 changes: 139 additions & 0 deletions jumble/src/components/FeedbackDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<void>;
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 (
<div className="fixed inset-0 bg-[#00000080] flex items-center justify-center z-50">
<div className="bg-white border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,0.5)] p-6 max-w-lg w-full mx-4">
<h2 className="text-2xl font-bold mb-4">
{initialScore === 1 ? "Positive Feedback" : "Improvement Feedback"}
</h2>
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label className="block text-sm font-medium mb-1">
{initialScore === 1
? "What did you like about this response?"
: "How could this response be improved?"}
</label>
<textarea
value={explanation}
onChange={(e) => setExplanation(e.target.value)}
className="w-full px-3 py-2 border-2 border-black"
rows={5}
disabled={isSubmitting}
placeholder={initialScore === 1
? "What aspects were helpful or well done?"
: "What would make this response more useful?"}
required
/>
</div>

<div className="flex justify-end gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 border-2 border-black hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-black text-white border-2 border-black hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
disabled={isSubmitting || !explanation.trim()}
>
{isSubmitting ? "Submitting..." : (
<span>
Submit <span className="text-xs">cmd+enter</span>
</span>
)}
</button>
</div>
</form>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions jumble/src/contexts/ActionManagerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type Action = {
priority?: number;
to?: string; // For NavLink actions
keyCombo?: KeyCombo; // Keyboard shortcut
className?: string; // Optional CSS class for styling the button
};

type ActionManagerContextType = {
Expand Down
3 changes: 3 additions & 0 deletions jumble/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ declare module "react-icons/md" {
export const MdOutlineStar: React.FC<IconProps>;
export const MdOutlineStarBorder: React.FC<IconProps>;
export const MdShare: React.FC<IconProps>;
export const MdSend: React.FC<IconProps>;
export const MdThumbDownOffAlt: React.FC<IconProps>;
export const MdThumbUpOffAlt: React.FC<IconProps>;
}
19 changes: 18 additions & 1 deletion jumble/src/hooks/use-global-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "@commontools/ui";
import { useLocation, useParams } from "react-router-dom";
import { type CharmRouteParams } from "@/routes.ts";
import { MdEdit, MdOutlineStar, MdShare } from "react-icons/md";
import { MdEdit, MdOutlineStar, MdSend, MdShare } from "react-icons/md";

import { useAction } from "@/contexts/ActionManagerContext.tsx";
import { useCallback, useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -92,4 +92,21 @@ export function useGlobalActions() {
[hasCharmId, togglePath],
),
);

// Feedback action (available only when a charm is open)
useAction(
useMemo(
() => ({
id: "feedback-toggle",
label: "Feedback",
icon: <MdSend size={24} />,
onClick: () => {
globalThis.dispatchEvent(new CustomEvent("toggle-feedback"));
},
priority: 30,
predicate: hasCharmId,
}),
[hasCharmId],
),
);
}
Loading