feat(FN-3139): add planning comment input to interview modals and show in c
Merges FN-3139 planning comment infrastructure (comment inputs in mission/milestone-slice/planning modals, comment display in conversation history), FN-3119 Quick Chat FAB with slash-triggered skill menu and plugin integration, FN-3117 plugin dashboard views and chat session icons, and FN-3066 plugi Fusion-Task-Id: FN-3139
This commit is contained in:
@@ -1759,6 +1759,7 @@ function SoulTab({
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [justSaved, setJustSaved] = useState(false);
|
const [justSaved, setJustSaved] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const justSavedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSoul(agent.soul ?? "");
|
setSoul(agent.soul ?? "");
|
||||||
@@ -1766,6 +1767,14 @@ function SoulTab({
|
|||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
}, [agent.id, agent.soul]);
|
}, [agent.id, agent.soul]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasChanges = soul !== (agent.soul ?? "");
|
const hasChanges = soul !== (agent.soul ?? "");
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -1779,7 +1788,10 @@ function SoulTab({
|
|||||||
await updateAgentSoul(agent.id, soul, projectId);
|
await updateAgentSoul(agent.id, soul, projectId);
|
||||||
addToast("Soul saved", "success");
|
addToast("Soul saved", "success");
|
||||||
setJustSaved(true);
|
setJustSaved(true);
|
||||||
setTimeout(() => setJustSaved(false), 3000);
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
justSavedTimeoutRef.current = setTimeout(() => setJustSaved(false), 3000);
|
||||||
await onSaved();
|
await onSaved();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addToast(`Failed to save soul: ${getErrorMessage(err)}`, "error");
|
addToast(`Failed to save soul: ${getErrorMessage(err)}`, "error");
|
||||||
@@ -1910,6 +1922,8 @@ function MemoryTab({
|
|||||||
const [savingSelectedFile, setSavingSelectedFile] = useState(false);
|
const [savingSelectedFile, setSavingSelectedFile] = useState(false);
|
||||||
const [selectedFileJustSaved, setSelectedFileJustSaved] = useState(false);
|
const [selectedFileJustSaved, setSelectedFileJustSaved] = useState(false);
|
||||||
const [fileSwitchHint, setFileSwitchHint] = useState("");
|
const [fileSwitchHint, setFileSwitchHint] = useState("");
|
||||||
|
const justSavedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const selectedFileJustSavedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const isReadOnly = agent.state === "running";
|
const isReadOnly = agent.state === "running";
|
||||||
const hasInlineChanges = memory !== (agent.memory ?? "");
|
const hasInlineChanges = memory !== (agent.memory ?? "");
|
||||||
@@ -1973,6 +1987,17 @@ function MemoryTab({
|
|||||||
void loadMemoryFiles();
|
void loadMemoryFiles();
|
||||||
}, [agent.id, agent.memory, loadMemoryFiles]);
|
}, [agent.id, agent.memory, loadMemoryFiles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (selectedFileJustSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(selectedFileJustSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSaveInlineMemory = async () => {
|
const handleSaveInlineMemory = async () => {
|
||||||
if (memory.length > 50000) {
|
if (memory.length > 50000) {
|
||||||
addToast("Memory must be at most 50,000 characters", "error");
|
addToast("Memory must be at most 50,000 characters", "error");
|
||||||
@@ -1984,7 +2009,10 @@ function MemoryTab({
|
|||||||
await updateAgentMemory(agent.id, memory, projectId);
|
await updateAgentMemory(agent.id, memory, projectId);
|
||||||
addToast("Memory saved", "success");
|
addToast("Memory saved", "success");
|
||||||
setJustSaved(true);
|
setJustSaved(true);
|
||||||
setTimeout(() => setJustSaved(false), 3000);
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
justSavedTimeoutRef.current = setTimeout(() => setJustSaved(false), 3000);
|
||||||
await onSaved();
|
await onSaved();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addToast(`Failed to save memory: ${getErrorMessage(err)}`, "error");
|
addToast(`Failed to save memory: ${getErrorMessage(err)}`, "error");
|
||||||
@@ -2016,7 +2044,10 @@ function MemoryTab({
|
|||||||
await saveAgentMemoryFile(agent.id, selectedFilePath, selectedFileContent, projectId);
|
await saveAgentMemoryFile(agent.id, selectedFilePath, selectedFileContent, projectId);
|
||||||
setSelectedFileDirty(false);
|
setSelectedFileDirty(false);
|
||||||
setSelectedFileJustSaved(true);
|
setSelectedFileJustSaved(true);
|
||||||
setTimeout(() => setSelectedFileJustSaved(false), 3000);
|
if (selectedFileJustSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(selectedFileJustSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
selectedFileJustSavedTimeoutRef.current = setTimeout(() => setSelectedFileJustSaved(false), 3000);
|
||||||
setFileSwitchHint("");
|
setFileSwitchHint("");
|
||||||
await loadMemoryFiles(selectedFilePath);
|
await loadMemoryFiles(selectedFilePath);
|
||||||
addToast("Agent memory file saved", "success");
|
addToast("Agent memory file saved", "success");
|
||||||
@@ -2255,6 +2286,8 @@ function InstructionsTab({
|
|||||||
const [isSavingFile, setIsSavingFile] = useState(false);
|
const [isSavingFile, setIsSavingFile] = useState(false);
|
||||||
const [justSaved, setJustSaved] = useState(false);
|
const [justSaved, setJustSaved] = useState(false);
|
||||||
const [justSavedFile, setJustSavedFile] = useState(false);
|
const [justSavedFile, setJustSavedFile] = useState(false);
|
||||||
|
const justSavedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const justSavedFileTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// Load file content when instructionsPath changes
|
// Load file content when instructionsPath changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2296,6 +2329,17 @@ function InstructionsTab({
|
|||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
}, [agent.id, agent.instructionsText, agent.instructionsPath]);
|
}, [agent.id, agent.instructionsText, agent.instructionsPath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (justSavedFileTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedFileTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasInstructionsChanges = (() => {
|
const hasInstructionsChanges = (() => {
|
||||||
const currentText = instructionsText ?? "";
|
const currentText = instructionsText ?? "";
|
||||||
const persistedText = agent.instructionsText ?? "";
|
const persistedText = agent.instructionsText ?? "";
|
||||||
@@ -2317,7 +2361,10 @@ function InstructionsTab({
|
|||||||
);
|
);
|
||||||
addToast("Instructions saved", "success");
|
addToast("Instructions saved", "success");
|
||||||
setJustSaved(true);
|
setJustSaved(true);
|
||||||
setTimeout(() => setJustSaved(false), 3000);
|
if (justSavedTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedTimeoutRef.current);
|
||||||
|
}
|
||||||
|
justSavedTimeoutRef.current = setTimeout(() => setJustSaved(false), 3000);
|
||||||
await onSaved();
|
await onSaved();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addToast(`Failed to save instructions: ${getErrorMessage(err)}`, "error");
|
addToast(`Failed to save instructions: ${getErrorMessage(err)}`, "error");
|
||||||
@@ -2339,7 +2386,10 @@ function InstructionsTab({
|
|||||||
addToast("Instructions file saved", "success");
|
addToast("Instructions file saved", "success");
|
||||||
setFileContentDirty(false);
|
setFileContentDirty(false);
|
||||||
setJustSavedFile(true);
|
setJustSavedFile(true);
|
||||||
setTimeout(() => setJustSavedFile(false), 3000);
|
if (justSavedFileTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedFileTimeoutRef.current);
|
||||||
|
}
|
||||||
|
justSavedFileTimeoutRef.current = setTimeout(() => setJustSavedFile(false), 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addToast(`Failed to save instructions file: ${getErrorMessage(err)}`, "error");
|
addToast(`Failed to save instructions file: ${getErrorMessage(err)}`, "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2602,6 +2652,7 @@ function HeartbeatProcedureSection({
|
|||||||
const [fileContentDirty, setFileContentDirty] = useState(false);
|
const [fileContentDirty, setFileContentDirty] = useState(false);
|
||||||
const [fileLoadError, setFileLoadError] = useState<string | null>(null);
|
const [fileLoadError, setFileLoadError] = useState<string | null>(null);
|
||||||
const [justSavedFile, setJustSavedFile] = useState(false);
|
const [justSavedFile, setJustSavedFile] = useState(false);
|
||||||
|
const justSavedFileTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const currentPath = agent.heartbeatProcedurePath?.trim();
|
const currentPath = agent.heartbeatProcedurePath?.trim();
|
||||||
const canonicalDefaultPath = `.fusion/agents/${agent.name
|
const canonicalDefaultPath = `.fusion/agents/${agent.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -2647,6 +2698,14 @@ function HeartbeatProcedureSection({
|
|||||||
setJustSavedFile(false);
|
setJustSavedFile(false);
|
||||||
}, [agent.id, currentPath]);
|
}, [agent.id, currentPath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (justSavedFileTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedFileTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleOpenViewer = async () => {
|
const handleOpenViewer = async () => {
|
||||||
if (!currentPath) return;
|
if (!currentPath) return;
|
||||||
setShowFileViewer(true);
|
setShowFileViewer(true);
|
||||||
@@ -2661,7 +2720,10 @@ function HeartbeatProcedureSection({
|
|||||||
setFileContentDirty(false);
|
setFileContentDirty(false);
|
||||||
setJustSavedFile(true);
|
setJustSavedFile(true);
|
||||||
addToast("Heartbeat procedure file saved", "success");
|
addToast("Heartbeat procedure file saved", "success");
|
||||||
setTimeout(() => setJustSavedFile(false), 3000);
|
if (justSavedFileTimeoutRef.current) {
|
||||||
|
clearTimeout(justSavedFileTimeoutRef.current);
|
||||||
|
}
|
||||||
|
justSavedFileTimeoutRef.current = setTimeout(() => setJustSavedFile(false), 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addToast(`Failed to save heartbeat procedure file: ${getErrorMessage(err)}`, "error");
|
addToast(`Failed to save heartbeat procedure file: ${getErrorMessage(err)}`, "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-comment {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-entry-thinking {
|
.conversation-entry-thinking {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ export function ConversationHistory({ entries, defaultShowThinking = false }: Co
|
|||||||
entry.question && responseValue !== undefined
|
entry.question && responseValue !== undefined
|
||||||
? formatResponse(entry.question, responseValue)
|
? formatResponse(entry.question, responseValue)
|
||||||
: "";
|
: "";
|
||||||
|
const responseRecord =
|
||||||
|
entry.response && typeof entry.response === "object" && !Array.isArray(entry.response)
|
||||||
|
? (entry.response as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const comment =
|
||||||
|
typeof responseRecord?._comment === "string" ? responseRecord._comment.trim() : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${entry.question?.id ?? "thinking"}-${index}`} className="conversation-entry">
|
<div key={`${entry.question?.id ?? "thinking"}-${index}`} className="conversation-entry">
|
||||||
@@ -120,6 +126,7 @@ export function ConversationHistory({ entries, defaultShowThinking = false }: Co
|
|||||||
<div className="conversation-entry-response">
|
<div className="conversation-entry-response">
|
||||||
<strong>Your response</strong>
|
<strong>Your response</strong>
|
||||||
<p>{formattedResponse || "—"}</p>
|
<p>{formattedResponse || "—"}</p>
|
||||||
|
{comment && <p className="conversation-comment">💬 {comment}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -627,20 +627,31 @@ interface InterviewQuestionFormProps {
|
|||||||
function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: InterviewQuestionFormProps) {
|
function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: InterviewQuestionFormProps) {
|
||||||
const [response, setResponse] = useState<QuestionResponse>({});
|
const [response, setResponse] = useState<QuestionResponse>({});
|
||||||
const [textValue, setTextValue] = useState("");
|
const [textValue, setTextValue] = useState("");
|
||||||
|
const [commentValue, setCommentValue] = useState("");
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
|
let nextResponse: QuestionResponse;
|
||||||
|
|
||||||
if (question.type === "text") {
|
if (question.type === "text") {
|
||||||
onSubmit({ [question.id]: textValue });
|
nextResponse = { [question.id]: textValue };
|
||||||
} else if (question.type === "confirm") {
|
} else if (question.type === "confirm") {
|
||||||
onSubmit({ [question.id]: response[question.id] === true });
|
nextResponse = { [question.id]: response[question.id] === true };
|
||||||
} else {
|
} else {
|
||||||
onSubmit(response);
|
nextResponse = response;
|
||||||
}
|
}
|
||||||
}, [question, response, textValue, onSubmit]);
|
|
||||||
|
const trimmedComment = commentValue.trim();
|
||||||
|
if (trimmedComment.length > 0) {
|
||||||
|
nextResponse = { ...nextResponse, _comment: trimmedComment };
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(nextResponse);
|
||||||
|
}, [commentValue, question, response, textValue, onSubmit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResponse({});
|
setResponse({});
|
||||||
setTextValue("");
|
setTextValue("");
|
||||||
|
setCommentValue("");
|
||||||
}, [question.id]);
|
}, [question.id]);
|
||||||
|
|
||||||
const isValid = () => {
|
const isValid = () => {
|
||||||
@@ -774,6 +785,22 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{question.type !== "text" && (
|
||||||
|
<div className="planning-comment-section">
|
||||||
|
<label className="planning-comment-label" htmlFor={`planning-comment-${question.id}`}>
|
||||||
|
Additional comments (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={`planning-comment-${question.id}`}
|
||||||
|
className="planning-textarea"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Add any extra context or direction..."
|
||||||
|
value={commentValue}
|
||||||
|
onChange={(e) => setCommentValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1028,20 +1028,31 @@ interface InterviewQuestionFormProps {
|
|||||||
function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: InterviewQuestionFormProps) {
|
function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }: InterviewQuestionFormProps) {
|
||||||
const [response, setResponse] = useState<QuestionResponse>({});
|
const [response, setResponse] = useState<QuestionResponse>({});
|
||||||
const [textValue, setTextValue] = useState("");
|
const [textValue, setTextValue] = useState("");
|
||||||
|
const [commentValue, setCommentValue] = useState("");
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
|
let nextResponse: QuestionResponse;
|
||||||
|
|
||||||
if (question.type === "text") {
|
if (question.type === "text") {
|
||||||
onSubmit({ [question.id]: textValue });
|
nextResponse = { [question.id]: textValue };
|
||||||
} else if (question.type === "confirm") {
|
} else if (question.type === "confirm") {
|
||||||
onSubmit({ [question.id]: response[question.id] === true });
|
nextResponse = { [question.id]: response[question.id] === true };
|
||||||
} else {
|
} else {
|
||||||
onSubmit(response);
|
nextResponse = response;
|
||||||
}
|
}
|
||||||
}, [question, response, textValue, onSubmit]);
|
|
||||||
|
const trimmedComment = commentValue.trim();
|
||||||
|
if (trimmedComment.length > 0) {
|
||||||
|
nextResponse = { ...nextResponse, _comment: trimmedComment };
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(nextResponse);
|
||||||
|
}, [commentValue, question, response, textValue, onSubmit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResponse({});
|
setResponse({});
|
||||||
setTextValue("");
|
setTextValue("");
|
||||||
|
setCommentValue("");
|
||||||
}, [question.id]);
|
}, [question.id]);
|
||||||
|
|
||||||
const isValid = () => {
|
const isValid = () => {
|
||||||
@@ -1175,6 +1186,22 @@ function InterviewQuestionForm({ question, progress, historyEntries, onSubmit }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{question.type !== "text" && (
|
||||||
|
<div className="planning-comment-section">
|
||||||
|
<label className="planning-comment-label" htmlFor={`planning-comment-${question.id}`}>
|
||||||
|
Additional comments (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={`planning-comment-${question.id}`}
|
||||||
|
className="planning-textarea"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Add any extra context or direction..."
|
||||||
|
value={commentValue}
|
||||||
|
onChange={(e) => setCommentValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -476,6 +476,21 @@
|
|||||||
box-shadow: var(--focus-ring);
|
box-shadow: var(--focus-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.planning-comment-section {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-comment-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-size-xs, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.planning-comment-section .planning-textarea {
|
||||||
|
min-height: calc(var(--space-md) * 5);
|
||||||
|
}
|
||||||
|
|
||||||
.planning-char-count {
|
.planning-char-count {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1248,6 +1263,10 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.planning-comment-section .planning-textarea {
|
||||||
|
min-height: calc(var(--space-md) * 5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Planning options compact on mobile */
|
/* Planning options compact on mobile */
|
||||||
.planning-option {
|
.planning-option {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
@@ -1522,21 +1522,32 @@ interface QuestionFormProps {
|
|||||||
function QuestionForm({ question, progress, historyEntries, onSubmit, onBack }: QuestionFormProps) {
|
function QuestionForm({ question, progress, historyEntries, onSubmit, onBack }: QuestionFormProps) {
|
||||||
const [response, setResponse] = useState<QuestionResponse>({});
|
const [response, setResponse] = useState<QuestionResponse>({});
|
||||||
const [textValue, setTextValue] = useState("");
|
const [textValue, setTextValue] = useState("");
|
||||||
|
const [commentValue, setCommentValue] = useState("");
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
|
let nextResponse: QuestionResponse;
|
||||||
|
|
||||||
if (question.type === "text") {
|
if (question.type === "text") {
|
||||||
onSubmit({ [question.id]: textValue });
|
nextResponse = { [question.id]: textValue };
|
||||||
} else if (question.type === "confirm") {
|
} else if (question.type === "confirm") {
|
||||||
onSubmit({ [question.id]: response[question.id] === true });
|
nextResponse = { [question.id]: response[question.id] === true };
|
||||||
} else {
|
} else {
|
||||||
onSubmit(response);
|
nextResponse = response;
|
||||||
}
|
}
|
||||||
}, [question, response, textValue, onSubmit]);
|
|
||||||
|
const trimmedComment = commentValue.trim();
|
||||||
|
if (trimmedComment.length > 0) {
|
||||||
|
nextResponse = { ...nextResponse, _comment: trimmedComment };
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(nextResponse);
|
||||||
|
}, [commentValue, question, response, textValue, onSubmit]);
|
||||||
|
|
||||||
// Reset state when question changes
|
// Reset state when question changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResponse({});
|
setResponse({});
|
||||||
setTextValue("");
|
setTextValue("");
|
||||||
|
setCommentValue("");
|
||||||
}, [question.id]);
|
}, [question.id]);
|
||||||
|
|
||||||
const isValid = () => {
|
const isValid = () => {
|
||||||
@@ -1670,6 +1681,22 @@ function QuestionForm({ question, progress, historyEntries, onSubmit, onBack }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{question.type !== "text" && (
|
||||||
|
<div className="planning-comment-section">
|
||||||
|
<label className="planning-comment-label" htmlFor={`planning-comment-${question.id}`}>
|
||||||
|
Additional comments (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={`planning-comment-${question.id}`}
|
||||||
|
className="planning-textarea"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Add any extra context or direction..."
|
||||||
|
value={commentValue}
|
||||||
|
onChange={(e) => setCommentValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,4 +72,19 @@ describe("ConversationHistory", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: /Show AI reasoning/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Show AI reasoning/i }));
|
||||||
expect(screen.getByText("Reasoning captured during subtask generation")).toBeDefined();
|
expect(screen.getByText("Reasoning captured during subtask generation")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders comment when response includes _comment", () => {
|
||||||
|
render(
|
||||||
|
<ConversationHistory
|
||||||
|
entries={[
|
||||||
|
{
|
||||||
|
question: baseQuestion,
|
||||||
|
response: { "q-scope": "small", _comment: "Need this done by next sprint" },
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("💬 Need this done by next sprint")).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -411,6 +411,52 @@ describe("MilestoneSliceInterviewModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("comment input", () => {
|
||||||
|
it("shows comment textarea and submits _comment in milestone interview", async () => {
|
||||||
|
mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MilestoneSliceInterviewModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onApplied={vi.fn()}
|
||||||
|
targetType="milestone"
|
||||||
|
targetId="MS-001"
|
||||||
|
targetTitle="Test Milestone"
|
||||||
|
projectId="test-project"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Start Interview"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Preparing next question/)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion(SAMPLE_QUESTION);
|
||||||
|
});
|
||||||
|
|
||||||
|
await screen.findByText("What is the target scope?");
|
||||||
|
expect(screen.getByPlaceholderText("Add any extra context or direction...")).toBeDefined();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("MVP"));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("Add any extra context or direction..."), {
|
||||||
|
target: { value: "Keep this aligned with mission MVP" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Continue/ }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRespondToMilestoneInterview).toHaveBeenCalledWith(
|
||||||
|
"session-123",
|
||||||
|
expect.objectContaining({ scope: "mvp", _comment: "Keep this aligned with mission MVP" }),
|
||||||
|
"test-project",
|
||||||
|
"test-tab-id",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resume session rehydration", () => {
|
describe("resume session rehydration", () => {
|
||||||
const mockSessionAwaitingInput = {
|
const mockSessionAwaitingInput = {
|
||||||
id: "session-resume-123",
|
id: "session-resume-123",
|
||||||
|
|||||||
@@ -292,4 +292,36 @@ describe("MissionInterviewModal", () => {
|
|||||||
expect(screen.getByText("Continuing...")).toBeInTheDocument();
|
expect(screen.getByText("Continuing...")).toBeInTheDocument();
|
||||||
expect(mockConnectMissionInterviewStream).toHaveBeenCalledTimes(2);
|
expect(mockConnectMissionInterviewStream).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows comment textarea and submits _comment for non-text questions", async () => {
|
||||||
|
renderModal();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("What do you want to build?"), {
|
||||||
|
target: { value: "Build a mission planning workflow" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Start Interview"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(streamHandlers).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
streamHandlers.onQuestion?.(SAMPLE_QUESTION);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByText("MVP"));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("Add any extra context or direction..."), {
|
||||||
|
target: { value: "Optimize for launch speed" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRespondToMissionInterview).toHaveBeenCalledWith(
|
||||||
|
"mission-session-1",
|
||||||
|
expect.objectContaining({ scope: "mvp", _comment: "Optimize for launch speed" }),
|
||||||
|
undefined,
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1279,6 +1279,125 @@ describe("PlanningModeModal", () => {
|
|||||||
expect(container.querySelector(".planning-question-form > .planning-actions")).not.toBeNull();
|
expect(container.querySelector(".planning-question-form > .planning-actions")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows comment textarea for single_select questions", async () => {
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||||
|
target: { value: "Build auth system" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Start Planning"));
|
||||||
|
|
||||||
|
expect(await screen.findByPlaceholderText("Add any extra context or direction...")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show comment textarea for text questions", async () => {
|
||||||
|
const textQuestion: PlanningQuestion = {
|
||||||
|
id: "q-text",
|
||||||
|
type: "text",
|
||||||
|
question: "Describe your requirements",
|
||||||
|
};
|
||||||
|
|
||||||
|
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handlers.onQuestion?.(textQuestion);
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: vi.fn(),
|
||||||
|
isConnected: vi.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||||
|
target: { value: "Build auth system" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Start Planning"));
|
||||||
|
|
||||||
|
await screen.findByText("Describe your requirements");
|
||||||
|
expect(screen.queryByPlaceholderText("Add any extra context or direction...")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes _comment in response when comment is filled", async () => {
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||||
|
target: { value: "Build auth system" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Start Planning"));
|
||||||
|
|
||||||
|
await screen.findByText("What is the scope?");
|
||||||
|
fireEvent.click(screen.getByText("Medium"));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("Add any extra context or direction..."), {
|
||||||
|
target: { value: "Prioritize API first" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRespondToPlanning).toHaveBeenCalledWith(
|
||||||
|
"session-123",
|
||||||
|
expect.objectContaining({ "q-scope": "medium", _comment: "Prioritize API first" }),
|
||||||
|
undefined,
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits _comment when comment is empty", async () => {
|
||||||
|
render(
|
||||||
|
<PlanningModeModal
|
||||||
|
isOpen={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
onTaskCreated={mockOnTaskCreated}
|
||||||
|
onTasksCreated={vi.fn()}
|
||||||
|
tasks={mockTasks}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
||||||
|
target: { value: "Build auth system" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText("Start Planning"));
|
||||||
|
|
||||||
|
await screen.findByText("What is the scope?");
|
||||||
|
fireEvent.click(screen.getByText("Medium"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRespondToPlanning).toHaveBeenCalledWith(
|
||||||
|
"session-123",
|
||||||
|
expect.not.objectContaining({ _comment: expect.anything() }),
|
||||||
|
undefined,
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shows reconnecting indicator without clearing current question state", async () => {
|
it("shows reconnecting indicator without clearing current question state", async () => {
|
||||||
let streamHandlers: any;
|
let streamHandlers: any;
|
||||||
|
|
||||||
|
|||||||
@@ -645,30 +645,42 @@ function formatResponseForAgent(
|
|||||||
responses: Record<string, unknown>
|
responses: Record<string, unknown>
|
||||||
): string {
|
): string {
|
||||||
const responseValue = responses[question.id];
|
const responseValue = responses[question.id];
|
||||||
|
const comment = typeof responses._comment === "string" ? responses._comment.trim() : "";
|
||||||
|
|
||||||
|
let formatted: string;
|
||||||
|
|
||||||
switch (question.type) {
|
switch (question.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "single_select":
|
case "single_select":
|
||||||
if (typeof responseValue === "string") {
|
if (typeof responseValue === "string") {
|
||||||
const option = question.options?.find((o) => o.id === responseValue);
|
const option = question.options?.find((o) => o.id === responseValue);
|
||||||
return `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "multi_select":
|
case "multi_select":
|
||||||
if (Array.isArray(responseValue)) {
|
if (Array.isArray(responseValue)) {
|
||||||
const selected = responseValue.map((id) => {
|
const selected = responseValue.map((id) => {
|
||||||
const option = question.options?.find((o) => o.id === id);
|
const option = question.options?.find((o) => o.id === id);
|
||||||
return option?.label || id;
|
return option?.label || id;
|
||||||
});
|
});
|
||||||
return `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "confirm":
|
case "confirm":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return comment.length > 0 ? `${formatted}\n\nAdditional context: ${comment}` : formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
||||||
@@ -735,15 +747,23 @@ function formatInterviewHistory(
|
|||||||
|
|
||||||
return history
|
return history
|
||||||
.map(({ question, response }) => {
|
.map(({ question, response }) => {
|
||||||
const responseValue =
|
const responseRecord =
|
||||||
response && typeof response === "object" && !Array.isArray(response)
|
response && typeof response === "object" && !Array.isArray(response)
|
||||||
? (response as Record<string, unknown>)[question.id]
|
? (response as Record<string, unknown>)
|
||||||
: response;
|
: undefined;
|
||||||
|
const responseValue = responseRecord ? responseRecord[question.id] : response;
|
||||||
|
const comment = typeof responseRecord?._comment === "string" ? responseRecord._comment.trim() : "";
|
||||||
|
|
||||||
return [
|
const lines = [
|
||||||
`Q: ${question.question}`,
|
`Q: ${question.question}`,
|
||||||
`A: ${typeof responseValue === "string" ? responseValue : JSON.stringify(responseValue ?? null)}`,
|
`A: ${typeof responseValue === "string" ? responseValue : JSON.stringify(responseValue ?? null)}`,
|
||||||
].join("\n");
|
];
|
||||||
|
|
||||||
|
if (comment.length > 0) {
|
||||||
|
lines.push(`Comment: ${comment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
})
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -703,30 +703,42 @@ function formatResponseForAgent(
|
|||||||
responses: Record<string, unknown>
|
responses: Record<string, unknown>
|
||||||
): string {
|
): string {
|
||||||
const responseValue = responses[question.id];
|
const responseValue = responses[question.id];
|
||||||
|
const comment = typeof responses._comment === "string" ? responses._comment.trim() : "";
|
||||||
|
|
||||||
|
let formatted: string;
|
||||||
|
|
||||||
switch (question.type) {
|
switch (question.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "single_select":
|
case "single_select":
|
||||||
if (typeof responseValue === "string") {
|
if (typeof responseValue === "string") {
|
||||||
const option = question.options?.find((o) => o.id === responseValue);
|
const option = question.options?.find((o) => o.id === responseValue);
|
||||||
return `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "multi_select":
|
case "multi_select":
|
||||||
if (Array.isArray(responseValue)) {
|
if (Array.isArray(responseValue)) {
|
||||||
const selected = responseValue.map((id) => {
|
const selected = responseValue.map((id) => {
|
||||||
const option = question.options?.find((o) => o.id === id);
|
const option = question.options?.find((o) => o.id === id);
|
||||||
return option?.label || id;
|
return option?.label || id;
|
||||||
});
|
});
|
||||||
return `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
case "confirm":
|
case "confirm":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return comment.length > 0 ? `${formatted}\n\nAdditional context: ${comment}` : formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
||||||
@@ -832,15 +844,23 @@ function formatMissionInterviewHistory(
|
|||||||
|
|
||||||
return history
|
return history
|
||||||
.map(({ question, response }) => {
|
.map(({ question, response }) => {
|
||||||
const responseValue =
|
const responseRecord =
|
||||||
response && typeof response === "object" && !Array.isArray(response)
|
response && typeof response === "object" && !Array.isArray(response)
|
||||||
? (response as Record<string, unknown>)[question.id]
|
? (response as Record<string, unknown>)
|
||||||
: response;
|
: undefined;
|
||||||
|
const responseValue = responseRecord ? responseRecord[question.id] : response;
|
||||||
|
const comment = typeof responseRecord?._comment === "string" ? responseRecord._comment.trim() : "";
|
||||||
|
|
||||||
return [
|
const lines = [
|
||||||
`Q: ${question.question}`,
|
`Q: ${question.question}`,
|
||||||
`A: ${typeof responseValue === "string" ? responseValue : JSON.stringify(responseValue ?? null)}`,
|
`A: ${typeof responseValue === "string" ? responseValue : JSON.stringify(responseValue ?? null)}`,
|
||||||
].join("\n");
|
];
|
||||||
|
|
||||||
|
if (comment.length > 0) {
|
||||||
|
lines.push(`Comment: ${comment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
})
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1593,17 +1593,23 @@ function formatResponseForAgent(
|
|||||||
responses: Record<string, unknown>
|
responses: Record<string, unknown>
|
||||||
): string {
|
): string {
|
||||||
const responseValue = responses[question.id];
|
const responseValue = responses[question.id];
|
||||||
|
const comment = typeof responses._comment === "string" ? responses._comment.trim() : "";
|
||||||
|
|
||||||
|
let formatted: string;
|
||||||
|
|
||||||
switch (question.type) {
|
switch (question.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
|
|
||||||
case "single_select":
|
case "single_select":
|
||||||
if (typeof responseValue === "string") {
|
if (typeof responseValue === "string") {
|
||||||
const option = question.options?.find((o) => o.id === responseValue);
|
const option = question.options?.find((o) => o.id === responseValue);
|
||||||
return `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${option?.label || responseValue}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
|
|
||||||
case "multi_select":
|
case "multi_select":
|
||||||
if (Array.isArray(responseValue)) {
|
if (Array.isArray(responseValue)) {
|
||||||
@@ -1611,16 +1617,22 @@ function formatResponseForAgent(
|
|||||||
const option = question.options?.find((o) => o.id === id);
|
const option = question.options?.find((o) => o.id === id);
|
||||||
return option?.label || id;
|
return option?.label || id;
|
||||||
});
|
});
|
||||||
return `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
formatted = `Question: ${question.question}\n\nSelected: ${selected.join(", ")}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue}`;
|
||||||
|
break;
|
||||||
|
|
||||||
case "confirm":
|
case "confirm":
|
||||||
return `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${responseValue === true ? "Yes" : "No"}`;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
formatted = `Question: ${question.question}\n\nAnswer: ${JSON.stringify(responseValue)}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return comment.length > 0 ? `${formatted}\n\nAdditional context: ${comment}` : formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
function coerceResponseRecord(question: PlanningQuestion, response: unknown): Record<string, unknown> {
|
||||||
@@ -1692,12 +1704,15 @@ export function formatInterviewQA(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entries = history.map(({ question, response }) => {
|
const entries = history.map(({ question, response }) => {
|
||||||
const responseValue =
|
const responseRecord =
|
||||||
response && typeof response === "object" && !Array.isArray(response)
|
response && typeof response === "object" && !Array.isArray(response)
|
||||||
? (response as Record<string, unknown>)[question.id]
|
? (response as Record<string, unknown>)
|
||||||
: response;
|
: undefined;
|
||||||
|
const responseValue = responseRecord ? responseRecord[question.id] : response;
|
||||||
|
const comment = typeof responseRecord?._comment === "string" ? responseRecord._comment.trim() : "";
|
||||||
|
|
||||||
return `**Q: ${question.question}**\nA: ${formatInterviewAnswer(question, responseValue)}`;
|
const answerLine = `**Q: ${question.question}**\nA: ${formatInterviewAnswer(question, responseValue)}`;
|
||||||
|
return comment.length > 0 ? `${answerLine}\nComment: ${comment}` : answerLine;
|
||||||
});
|
});
|
||||||
|
|
||||||
return `## Planning Interview Context\n\n${entries.join("\n\n")}`;
|
return `## Planning Interview Context\n\n${entries.join("\n\n")}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user