diff --git a/components/frontend/.env.example b/components/frontend/.env.example
index 2af227336..c3e769310 100644
--- a/components/frontend/.env.example
+++ b/components/frontend/.env.example
@@ -31,3 +31,10 @@ MAX_UPLOAD_SIZE_DOCUMENTS=716800
MAX_UPLOAD_SIZE_IMAGES=3145728
IMAGE_COMPRESSION_TARGET=358400
+
+# Langfuse Configuration for User Feedback
+# These are used by the /api/feedback route to submit user feedback scores
+# Get your keys from your Langfuse instance: Settings > API Keys
+# LANGFUSE_HOST=https://langfuse-langfuse.apps.rosa.vteam-uat.0ksl.p3.openshiftapps.com
+# LANGFUSE_PUBLIC_KEY=pk-lf-YOUR-PUBLIC-KEY-HERE
+# LANGFUSE_SECRET_KEY=sk-lf-YOUR-SECRET-KEY-HERE
diff --git a/components/frontend/package-lock.json b/components/frontend/package-lock.json
index 8b9a5523e..02a06fef8 100644
--- a/components/frontend/package-lock.json
+++ b/components/frontend/package-lock.json
@@ -27,6 +27,7 @@
"date-fns": "^4.1.0",
"file-type": "^21.1.1",
"highlight.js": "^11.11.1",
+ "langfuse": "^3.38.6",
"lucide-react": "^0.542.0",
"next": "15.5.9",
"next-themes": "^0.4.6",
@@ -5933,6 +5934,30 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/langfuse": {
+ "version": "3.38.6",
+ "resolved": "https://registry.npmjs.org/langfuse/-/langfuse-3.38.6.tgz",
+ "integrity": "sha512-mtwfsNGIYvObRh+NYNGlJQJDiBN+Wr3Hnr++wN25mxuOpSTdXX+JQqVCyAqGL5GD2TAXRZ7COsN42Vmp9krYmg==",
+ "license": "MIT",
+ "dependencies": {
+ "langfuse-core": "^3.38.6"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/langfuse-core": {
+ "version": "3.38.6",
+ "resolved": "https://registry.npmjs.org/langfuse-core/-/langfuse-core-3.38.6.tgz",
+ "integrity": "sha512-EcZXa+DK9FJdi1I30+u19eKjuBJ04du6j2Nybk19KKCuraLczg/ppkTQcGvc4QOk//OAi3qUHrajUuV74RXsBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mustache": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -7232,6 +7257,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "license": "MIT",
+ "bin": {
+ "mustache": "bin/mustache"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
diff --git a/components/frontend/package.json b/components/frontend/package.json
index 7f0109453..9f34e55a7 100644
--- a/components/frontend/package.json
+++ b/components/frontend/package.json
@@ -28,6 +28,7 @@
"date-fns": "^4.1.0",
"file-type": "^21.1.1",
"highlight.js": "^11.11.1",
+ "langfuse": "^3.38.6",
"lucide-react": "^0.542.0",
"next": "15.5.9",
"next-themes": "^0.4.6",
diff --git a/components/frontend/src/app/api/feedback/route.ts b/components/frontend/src/app/api/feedback/route.ts
new file mode 100644
index 000000000..1049d255a
--- /dev/null
+++ b/components/frontend/src/app/api/feedback/route.ts
@@ -0,0 +1,156 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+/**
+ * POST /api/feedback
+ *
+ * Sends user feedback to Langfuse as a score.
+ * This route acts as a proxy to protect the Langfuse secret key.
+ *
+ * Request body:
+ * - traceId: string (optional - if we have a trace ID from the session)
+ * - value: number (1 for positive, 0 for negative)
+ * - comment?: string (optional user comment)
+ * - username: string
+ * - projectName: string
+ * - sessionName: string
+ * - context?: string (what the user was working on)
+ * - includeTranscript?: boolean
+ * - transcript?: Array<{ role: string; content: string; timestamp?: string }>
+ */
+
+type FeedbackRequest = {
+ traceId?: string;
+ value: number;
+ comment?: string;
+ username: string;
+ projectName: string;
+ sessionName: string;
+ context?: string;
+ includeTranscript?: boolean;
+ transcript?: Array<{ role: string; content: string; timestamp?: string }>;
+};
+
+export async function POST(request: NextRequest) {
+ try {
+ const body: FeedbackRequest = await request.json();
+
+ const {
+ traceId,
+ value,
+ comment,
+ username,
+ projectName,
+ sessionName,
+ context,
+ includeTranscript,
+ transcript,
+ } = body;
+
+ // Validate required fields
+ if (typeof value !== 'number' || !username || !projectName || !sessionName) {
+ return NextResponse.json(
+ { error: 'Missing required fields: value, username, projectName, sessionName' },
+ { status: 400 }
+ );
+ }
+
+ // Get Langfuse configuration from environment
+ const publicKey = process.env.LANGFUSE_PUBLIC_KEY;
+ const secretKey = process.env.LANGFUSE_SECRET_KEY;
+ const host = process.env.LANGFUSE_HOST || process.env.NEXT_PUBLIC_LANGFUSE_HOST;
+
+ if (!publicKey || !secretKey || !host) {
+ console.warn('Langfuse not configured - feedback will not be recorded');
+ return NextResponse.json({
+ success: false,
+ message: 'Langfuse not configured'
+ });
+ }
+
+ // Build the feedback comment with context
+ const feedbackParts: string[] = [];
+
+ if (comment) {
+ feedbackParts.push(`User Comment: ${comment}`);
+ }
+
+ feedbackParts.push(`Project: ${projectName}`);
+ feedbackParts.push(`Session: ${sessionName}`);
+ feedbackParts.push(`User: ${username}`);
+
+ if (context) {
+ feedbackParts.push(`Context: ${context}`);
+ }
+
+ if (includeTranscript && transcript && transcript.length > 0) {
+ // Limit transcript to last 10 messages to avoid huge payloads
+ const recentMessages = transcript.slice(-10);
+ const transcriptSummary = recentMessages
+ .map(m => `[${m.role}]: ${m.content.substring(0, 200)}${m.content.length > 200 ? '...' : ''}`)
+ .join('\n');
+ feedbackParts.push(`\nRecent Transcript:\n${transcriptSummary}`);
+ }
+
+ const fullComment = feedbackParts.join('\n');
+
+ // Prepare the score payload for Langfuse
+ // If we don't have a traceId, we create a standalone score event
+ const scorePayload = {
+ name: 'user-feedback',
+ value: value,
+ comment: fullComment,
+ // Include metadata for filtering in Langfuse
+ dataType: 'NUMERIC' as const,
+ };
+
+ // If we have a traceId, attach the score to that trace
+ // Otherwise, we create the score and associate with session metadata
+ const endpoint = traceId
+ ? `${host}/api/public/scores`
+ : `${host}/api/public/scores`;
+
+ const payload = traceId
+ ? { ...scorePayload, traceId }
+ : {
+ ...scorePayload,
+ // When no traceId, include identifying metadata
+ traceId: `feedback-${sessionName}-${Date.now()}`,
+ };
+
+ // Send to Langfuse API
+ const authHeader = Buffer.from(`${publicKey}:${secretKey}`).toString('base64');
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Basic ${authHeader}`,
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error('Langfuse API error:', response.status, errorText);
+ return NextResponse.json(
+ { error: 'Failed to submit feedback to Langfuse' },
+ { status: 500 }
+ );
+ }
+
+ const result = await response.json();
+
+ return NextResponse.json({
+ success: true,
+ scoreId: result.id,
+ message: 'Feedback submitted successfully'
+ });
+
+ } catch (error) {
+ console.error('Error submitting feedback:', error);
+ return NextResponse.json(
+ { error: 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
index 355b08ae1..d86052df6 100644
--- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
+++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
@@ -82,6 +82,7 @@ import {
useDeleteSession,
useContinueSession,
useReposStatus,
+ useCurrentUser,
} from "@/services/queries";
import {
useWorkspaceList,
@@ -93,6 +94,7 @@ import {
} from "@/services/queries/use-workflows";
import { useProjectIntegrationStatus } from "@/services/queries/use-projects";
import { useMutation } from "@tanstack/react-query";
+import { FeedbackProvider } from "@/contexts/FeedbackContext";
// Constants for artifact auto-refresh timing
// Moved outside component to avoid unnecessary effect re-runs
@@ -187,6 +189,9 @@ export default function ProjectSessionDetailPage({
// Check integration status
const { data: integrationStatus } = useProjectIntegrationStatus(projectName);
const githubConfigured = integrationStatus?.github ?? false;
+
+ // Get current user for feedback context
+ const { data: currentUser } = useCurrentUser();
// Extract phase for sidebar state management
const phase = session?.status?.phase || "Pending";
@@ -1966,37 +1971,47 @@ export default function ProjectSessionDetailPage({
)}
- Promise.resolve(sendChat())}
- onInterrupt={aguiInterrupt}
- onEndSession={() => Promise.resolve(handleEndSession())}
- onGoToResults={() => {}}
- onContinue={handleContinue}
- workflowMetadata={workflowMetadata}
- onCommandClick={handleCommandClick}
- isRunActive={isRunActive}
- showWelcomeExperience={!["Completed", "Failed", "Stopped", "Stopping"].includes(session?.status?.phase || "")}
- activeWorkflow={workflowManagement.activeWorkflow}
- userHasInteracted={userHasInteracted}
- queuedMessages={sessionQueue.messages}
- hasRealMessages={hasRealMessages}
- welcomeExperienceComponent={
- setUserHasInteracted(true)}
- userHasInteracted={userHasInteracted}
- sessionPhase={session?.status?.phase}
- hasRealMessages={hasRealMessages}
- onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)}
- selectedWorkflow={workflowManagement.selectedWorkflow}
- />
- }
- />
+
+ Promise.resolve(sendChat())}
+ onInterrupt={aguiInterrupt}
+ onEndSession={() => Promise.resolve(handleEndSession())}
+ onGoToResults={() => {}}
+ onContinue={handleContinue}
+ workflowMetadata={workflowMetadata}
+ onCommandClick={handleCommandClick}
+ isRunActive={isRunActive}
+ showWelcomeExperience={!["Completed", "Failed", "Stopped", "Stopping"].includes(session?.status?.phase || "")}
+ activeWorkflow={workflowManagement.activeWorkflow}
+ userHasInteracted={userHasInteracted}
+ queuedMessages={sessionQueue.messages}
+ hasRealMessages={hasRealMessages}
+ welcomeExperienceComponent={
+ setUserHasInteracted(true)}
+ userHasInteracted={userHasInteracted}
+ sessionPhase={session?.status?.phase}
+ hasRealMessages={hasRealMessages}
+ onLoadWorkflow={() => setCustomWorkflowDialogOpen(true)}
+ selectedWorkflow={workflowManagement.selectedWorkflow}
+ />
+ }
+ />
+
diff --git a/components/frontend/src/components/feedback/FeedbackButtons.tsx b/components/frontend/src/components/feedback/FeedbackButtons.tsx
new file mode 100644
index 000000000..42ad9bade
--- /dev/null
+++ b/components/frontend/src/components/feedback/FeedbackButtons.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import React, { useState } from "react";
+import { ThumbsUp, ThumbsDown, Check } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { FeedbackModal, FeedbackType } from "./FeedbackModal";
+import { useFeedbackContextOptional } from "@/contexts/FeedbackContext";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+type FeedbackButtonsProps = {
+ messageContent?: string;
+ messageTimestamp?: string;
+ className?: string;
+};
+
+export function FeedbackButtons({
+ messageContent,
+ messageTimestamp,
+ className,
+}: FeedbackButtonsProps) {
+ const [feedbackModalOpen, setFeedbackModalOpen] = useState(false);
+ const [selectedFeedback, setSelectedFeedback] = useState(null);
+ const [submittedFeedback, setSubmittedFeedback] = useState(null);
+
+ const feedbackContext = useFeedbackContextOptional();
+
+ // Don't render if no context available
+ if (!feedbackContext) {
+ return null;
+ }
+
+ const handleFeedbackClick = (type: FeedbackType) => {
+ // If already submitted this feedback type, do nothing
+ if (submittedFeedback === type) {
+ return;
+ }
+
+ setSelectedFeedback(type);
+ setFeedbackModalOpen(true);
+ };
+
+ const handleSubmitSuccess = () => {
+ setSubmittedFeedback(selectedFeedback);
+ };
+
+ const isPositiveSubmitted = submittedFeedback === "positive";
+ const isNegativeSubmitted = submittedFeedback === "negative";
+
+ return (
+ <>
+
+
+ {/* Thumbs Up Button */}
+
+
+
+
+
+ {isPositiveSubmitted ? "Thanks for your feedback!" : "This was helpful"}
+
+
+
+ {/* Thumbs Down Button */}
+
+
+
+
+
+ {isNegativeSubmitted ? "Thanks for your feedback!" : "This wasn't helpful"}
+
+
+
+
+
+ {/* Feedback Modal */}
+ {selectedFeedback && (
+
+ )}
+ >
+ );
+}
diff --git a/components/frontend/src/components/feedback/FeedbackModal.tsx b/components/frontend/src/components/feedback/FeedbackModal.tsx
new file mode 100644
index 000000000..bca9881e5
--- /dev/null
+++ b/components/frontend/src/components/feedback/FeedbackModal.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import { ThumbsUp, ThumbsDown, Loader2, AlertTriangle } from "lucide-react";
+import { useFeedbackContextOptional } from "@/contexts/FeedbackContext";
+import type { MessageObject, ToolUseMessages } from "@/types/agentic-session";
+
+export type FeedbackType = "positive" | "negative";
+
+type FeedbackModalProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ feedbackType: FeedbackType;
+ messageContent?: string;
+ messageTimestamp?: string;
+ onSubmitSuccess?: () => void;
+};
+
+// Helper to extract text content from messages
+function extractMessageText(
+ messages: Array
+): Array<{ role: string; content: string; timestamp?: string }> {
+ return messages
+ .filter((m): m is MessageObject => "type" in m && m.type !== undefined)
+ .filter((m) => m.type === "user_message" || m.type === "agent_message")
+ .map((m) => {
+ let content = "";
+ if (typeof m.content === "string") {
+ content = m.content;
+ } else if ("text" in m.content) {
+ content = m.content.text;
+ } else if ("thinking" in m.content) {
+ content = m.content.thinking;
+ }
+ return {
+ role: m.type === "user_message" ? "user" : "assistant",
+ content,
+ timestamp: m.timestamp,
+ };
+ });
+}
+
+export function FeedbackModal({
+ open,
+ onOpenChange,
+ feedbackType,
+ messageContent,
+ onSubmitSuccess,
+}: FeedbackModalProps) {
+ const [comment, setComment] = useState("");
+ const [includeTranscript, setIncludeTranscript] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const feedbackContext = useFeedbackContextOptional();
+
+ const handleSubmit = async () => {
+ if (!feedbackContext) {
+ setError("Session context not available");
+ return;
+ }
+
+ setIsSubmitting(true);
+ setError(null);
+
+ try {
+ // Build context string from what the user was working on
+ const contextParts: string[] = [];
+
+ if (feedbackContext.initialPrompt) {
+ contextParts.push(`Initial prompt: ${feedbackContext.initialPrompt.substring(0, 200)}`);
+ }
+
+ if (feedbackContext.activeWorkflow) {
+ contextParts.push(`Workflow: ${feedbackContext.activeWorkflow}`);
+ }
+
+ if (messageContent) {
+ contextParts.push(`Response being rated: ${messageContent.substring(0, 500)}`);
+ }
+
+ const transcript = includeTranscript
+ ? extractMessageText(feedbackContext.messages)
+ : undefined;
+
+ const response = await fetch("/api/feedback", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ value: feedbackType === "positive" ? 1 : 0,
+ comment: comment || undefined,
+ username: feedbackContext.username,
+ projectName: feedbackContext.projectName,
+ sessionName: feedbackContext.sessionName,
+ context: contextParts.join("; "),
+ includeTranscript,
+ transcript,
+ traceId: feedbackContext.traceId,
+ }),
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || "Failed to submit feedback");
+ }
+
+ // Success - close modal and reset
+ setComment("");
+ setIncludeTranscript(false);
+ onOpenChange(false);
+ onSubmitSuccess?.();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to submit feedback");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setComment("");
+ setIncludeTranscript(false);
+ setError(null);
+ onOpenChange(false);
+ };
+
+ const isPositive = feedbackType === "positive";
+
+ return (
+
+ );
+}
diff --git a/components/frontend/src/components/feedback/index.ts b/components/frontend/src/components/feedback/index.ts
new file mode 100644
index 000000000..fe2338908
--- /dev/null
+++ b/components/frontend/src/components/feedback/index.ts
@@ -0,0 +1,3 @@
+export { FeedbackButtons } from "./FeedbackButtons";
+export { FeedbackModal } from "./FeedbackModal";
+export type { FeedbackType } from "./FeedbackModal";
diff --git a/components/frontend/src/components/ui/message.tsx b/components/frontend/src/components/ui/message.tsx
index 398858e87..0550f2aea 100644
--- a/components/frontend/src/components/ui/message.tsx
+++ b/components/frontend/src/components/ui/message.tsx
@@ -21,6 +21,8 @@ export type MessageProps = {
actions?: React.ReactNode;
timestamp?: string;
streaming?: boolean;
+ /** Feedback buttons to show below the message (for bot messages) */
+ feedbackButtons?: React.ReactNode;
};
const defaultComponents: Components = {
@@ -172,7 +174,7 @@ export const LoadingDots = () => {
export const Message = React.forwardRef(
(
- { role, content, isLoading, className, components, borderless, actions, timestamp, streaming, ...props },
+ { role, content, isLoading, className, components, borderless, actions, timestamp, streaming, feedbackButtons, ...props },
ref
) => {
const isBot = role === "bot";
@@ -237,6 +239,13 @@ export const Message = React.forwardRef(
)}
+ {/* Feedback buttons for bot messages */}
+ {isBot && feedbackButtons && !isLoading && !streaming && (
+
+ {feedbackButtons}
+
+ )}
+
{actions ? (
{actions}
) : null}
diff --git a/components/frontend/src/components/ui/stream-message.tsx b/components/frontend/src/components/ui/stream-message.tsx
index 77cbab175..5b3f8d2a5 100644
--- a/components/frontend/src/components/ui/stream-message.tsx
+++ b/components/frontend/src/components/ui/stream-message.tsx
@@ -7,6 +7,7 @@ import { ToolMessage } from "@/components/ui/tool-message";
import { ThinkingMessage } from "@/components/ui/thinking-message";
import { SystemMessage } from "@/components/ui/system-message";
import { Button } from "@/components/ui/button";
+import { FeedbackButtons } from "@/components/feedback";
export type StreamMessageProps = {
message: (MessageObject | ToolUseMessages | HierarchicalToolMessage) & { streaming?: boolean };
@@ -63,14 +64,52 @@ export const StreamMessage: React.FC = ({ message, onGoToRes
case "user_message":
case "agent_message": {
const isStreaming = 'streaming' in message && message.streaming;
+ const isAgent = m.type === "agent_message";
+
+ // Get content text for feedback context
+ const getContentText = () => {
+ if (typeof m.content === "string") return m.content;
+ if ("text" in m.content) return m.content.text;
+ if ("thinking" in m.content) return m.content.thinking;
+ return "";
+ };
+
+ // Feedback buttons for agent text messages (not tool use/result, not streaming)
+ const feedbackElement = isAgent && !isStreaming ? (
+
+ ) : undefined;
+
if (typeof m.content === "string") {
- return ;
+ return (
+
+ );
}
switch (m.content.type) {
case "thinking_block":
return
case "text_block":
- return
+ return (
+
+ );
case "tool_use_block":
return
case "tool_result_block":
diff --git a/components/frontend/src/contexts/FeedbackContext.tsx b/components/frontend/src/contexts/FeedbackContext.tsx
new file mode 100644
index 000000000..e6df07f17
--- /dev/null
+++ b/components/frontend/src/contexts/FeedbackContext.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import React, { createContext, useContext, useMemo } from "react";
+import type { MessageObject, ToolUseMessages } from "@/types/agentic-session";
+
+export type FeedbackContextValue = {
+ projectName: string;
+ sessionName: string;
+ username: string;
+ initialPrompt?: string;
+ activeWorkflow?: string;
+ messages: Array;
+ // traceId from Langfuse if available (from session status)
+ traceId?: string;
+};
+
+const FeedbackContext = createContext(null);
+
+type FeedbackProviderProps = {
+ projectName: string;
+ sessionName: string;
+ username: string;
+ initialPrompt?: string;
+ activeWorkflow?: string;
+ messages: Array;
+ traceId?: string;
+ children: React.ReactNode;
+};
+
+export function FeedbackProvider({
+ projectName,
+ sessionName,
+ username,
+ initialPrompt,
+ activeWorkflow,
+ messages,
+ traceId,
+ children,
+}: FeedbackProviderProps) {
+ const value = useMemo(
+ () => ({
+ projectName,
+ sessionName,
+ username,
+ initialPrompt,
+ activeWorkflow,
+ messages,
+ traceId,
+ }),
+ [projectName, sessionName, username, initialPrompt, activeWorkflow, messages, traceId]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useFeedbackContext() {
+ const context = useContext(FeedbackContext);
+ if (!context) {
+ throw new Error("useFeedbackContext must be used within a FeedbackProvider");
+ }
+ return context;
+}
+
+// Optional hook that doesn't throw if context is missing
+export function useFeedbackContextOptional() {
+ return useContext(FeedbackContext);
+}
diff --git a/components/manifests/base/frontend-deployment.yaml b/components/manifests/base/frontend-deployment.yaml
index 22570cf93..62953b93b 100644
--- a/components/manifests/base/frontend-deployment.yaml
+++ b/components/manifests/base/frontend-deployment.yaml
@@ -31,6 +31,26 @@ spec:
value: "ambient-code"
- name: VTEAM_VERSION
value: "v0.0.7"
+ # Langfuse configuration for user feedback collection
+ # Reads from ambient-admin-langfuse-secret if available
+ - name: LANGFUSE_HOST
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_HOST
+ optional: true
+ - name: LANGFUSE_PUBLIC_KEY
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_PUBLIC_KEY
+ optional: true
+ - name: LANGFUSE_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_SECRET_KEY
+ optional: true
resources:
requests:
cpu: 100m
diff --git a/components/manifests/overlays/local-dev/frontend-patch.yaml b/components/manifests/overlays/local-dev/frontend-patch.yaml
index c88831715..70a710f53 100644
--- a/components/manifests/overlays/local-dev/frontend-patch.yaml
+++ b/components/manifests/overlays/local-dev/frontend-patch.yaml
@@ -25,6 +25,25 @@ spec:
key: token
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: "0"
+ # Langfuse configuration for user feedback collection
+ - name: LANGFUSE_HOST
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_HOST
+ optional: true
+ - name: LANGFUSE_PUBLIC_KEY
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_PUBLIC_KEY
+ optional: true
+ - name: LANGFUSE_SECRET_KEY
+ valueFrom:
+ secretKeyRef:
+ name: ambient-admin-langfuse-secret
+ key: LANGFUSE_SECRET_KEY
+ optional: true
resources:
limits:
memory: "1Gi"