Add feedback UI (#264)

Co-authored-by: Shubham Naik <shub@memgpt.ai>
This commit is contained in:
Shubham Naik
2025-12-17 13:41:22 -08:00
committed by GitHub
parent e53b0630a0
commit d4682421b6
3 changed files with 172 additions and 0 deletions

View File

@@ -48,6 +48,7 @@ import { AssistantMessage } from "./components/AssistantMessageRich";
import { CommandMessage } from "./components/CommandMessage";
import { EnterPlanModeDialog } from "./components/EnterPlanModeDialog";
import { ErrorMessage } from "./components/ErrorMessageRich";
import { FeedbackDialog } from "./components/FeedbackDialog";
import { Input } from "./components/InputRich";
import { MessageSearch } from "./components/MessageSearch";
import { ModelSelector } from "./components/ModelSelector";
@@ -391,6 +392,7 @@ export default function App({
| "profile"
| "search"
| "subagent"
| "feedback"
| null;
const [activeOverlay, setActiveOverlay] = useState<ActiveOverlay>(null);
const closeOverlay = useCallback(() => setActiveOverlay(null), []);
@@ -2536,6 +2538,12 @@ ${recentCommits}
return { submitted: true };
}
// Special handling for /feedback command - open feedback dialog
if (trimmed === "/feedback") {
setActiveOverlay("feedback");
return { submitted: true };
}
// Immediately add command to transcript with "running" phase
const cmdId = uid("cmd");
buffersRef.current.byId.set(cmdId, {
@@ -3380,6 +3388,81 @@ ${recentCommits}
);
// Handle escape when profile confirmation is pending
const handleFeedbackSubmit = useCallback(
async (message: string) => {
closeOverlay();
await withCommandLock(async () => {
const cmdId = uid("cmd");
try {
// Immediately add command to transcript with "running" phase
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: "/feedback",
output: "Sending feedback...",
phase: "running",
});
buffersRef.current.order.push(cmdId);
refreshDerived();
const settings = settingsManager.getSettings();
const baseURL =
process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL ||
"https://api.letta.com";
const apiKey =
process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
// Send feedback request manually since it's not in the SDK
const response = await fetch(`${baseURL}/v1/metadata/feedback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Letta-Source": "letta-code",
},
body: JSON.stringify({
message: message,
feature: "letta-code",
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to send feedback (${response.status}): ${errorText}`,
);
}
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: "/feedback",
output:
"Thank you for your feedback! Your message has been sent to the Letta team.",
phase: "finished",
success: true,
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: "/feedback",
output: `Failed to send feedback: ${errorDetails}`,
phase: "finished",
success: false,
});
refreshDerived();
}
});
},
[agentId, refreshDerived, withCommandLock, closeOverlay],
);
const handleProfileEscapeCancel = useCallback(() => {
if (profileConfirmPending) {
const { cmdId, name } = profileConfirmPending;
@@ -3962,6 +4045,14 @@ Plan file path: ${planFilePath}`;
<MessageSearch onClose={closeOverlay} />
)}
{/* Feedback Dialog - conditionally mounted as overlay */}
{activeOverlay === "feedback" && (
<FeedbackDialog
onSubmit={handleFeedbackSubmit}
onCancel={closeOverlay}
/>
)}
{/* Plan Mode Dialog - for ExitPlanMode tool */}
{currentApproval?.toolName === "ExitPlanMode" && (
<>

View File

@@ -166,6 +166,13 @@ export const commands: Record<string, Command> = {
return "Opening subagent manager...";
},
},
"/feedback": {
desc: "Send feedback to the Letta team",
handler: () => {
// Handled specially in App.tsx to send feedback request
return "Sending feedback...";
},
},
};
/**

View File

@@ -0,0 +1,74 @@
import { Box, Text, useInput } from "ink";
import { useState } from "react";
import { colors } from "./colors";
import { PasteAwareTextInput } from "./PasteAwareTextInput";
interface FeedbackDialogProps {
onSubmit: (message: string) => void;
onCancel: () => void;
}
export function FeedbackDialog({ onSubmit, onCancel }: FeedbackDialogProps) {
const [feedbackText, setFeedbackText] = useState("");
const [error, setError] = useState("");
useInput((input, key) => {
if (key.escape) {
onCancel();
}
});
const handleSubmit = (text: string) => {
const trimmed = text.trim();
if (!trimmed) {
setError("Feedback message cannot be empty");
return;
}
if (trimmed.length > 10000) {
setError("Feedback message is too long (max 10,000 characters)");
return;
}
onSubmit(trimmed);
};
return (
<Box flexDirection="column" paddingY={1}>
<Box marginBottom={1}>
<Text color={colors.approval.header} bold>
Send Feedback to Letta Team
</Text>
</Box>
<Box marginBottom={1}>
<Text dimColor>
Share your thoughts, report issues, or suggest improvements.
</Text>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Box marginBottom={1}>
<Text>Enter your feedback:</Text>
</Box>
<Box>
<Text color={colors.approval.header}>&gt; </Text>
<PasteAwareTextInput
value={feedbackText}
onChange={setFeedbackText}
onSubmit={handleSubmit}
placeholder="Type your feedback here..."
/>
</Box>
</Box>
{error && (
<Box marginBottom={1}>
<Text color="red">{error}</Text>
</Box>
)}
<Box>
<Text dimColor>Press Enter to submit Esc to cancel</Text>
</Box>
</Box>
);
}