Files
platform/src/workspaces/apps/Chat/QuestionActivity.tsx
T
pastilhas ba51ee0320 Complete frontend migration to unified Pi harness (Phase 7 + Phase 9)
- Delete legacy hooks: useClaude.ts, useOpenCode.ts, usePiMono.ts, App.backup.tsx
- Update all components to use usePi instead of legacy hooks
- Replace useVisiblePiMonoModels/useClaudeModels/useOpenCodeModels with useVisiblePiModels
- Migrate from LegacyChatMessage to ChatMessage type throughout
- Update SessionBar to remove provider and archive props
- Simplify ChatDetailPanel to Pi-only (remove Claude/OpenCode components)
- Fix useChatSessions calls (remove provider parameter)
- Update user-settings types: provider now only 'pi' instead of legacy values
- Update PI_HARNESS_REBUILD.md to mark phases complete
2026-02-20 21:42:26 +00:00

165 lines
6.0 KiB
TypeScript

import { useState } from 'react';
import { MessageCircleQuestion, Check } from 'lucide-react';
import type { ChatMessage } from './types';
type ToolMessage = Extract<ChatMessage, { role: 'tool' }>;
type QuestionOption = {
label: string;
description: string;
};
type Question = {
question: string;
header: string;
multiple: boolean;
options: QuestionOption[];
};
type QuestionActivityProps = {
message: ToolMessage;
onAnswer: (text: string) => void;
};
export const QuestionActivity = ({ message, onAnswer }: QuestionActivityProps) => {
const [selectedOptions, setSelectedOptions] = useState<Set<string>>(new Set());
const [otherText, setOtherText] = useState('');
const [answered, setAnswered] = useState(false);
const [answeredText, setAnsweredText] = useState('');
const input = message.toolInput as { questions?: Question[] };
const questions = input.questions;
if (!questions || questions.length === 0) return null;
const pending = message.output === undefined;
const handleSelect = (question: Question, label: string) => {
if (answered || !pending) return;
if (question.multiple) {
setSelectedOptions((prev) => {
const next = new Set(prev);
if (next.has(label)) next.delete(label);
else next.add(label);
return next;
});
} else {
const text = label;
setAnswered(true);
setAnsweredText(text);
onAnswer(text);
}
};
const handleSubmitMultiple = () => {
if (selectedOptions.size === 0 || answered || !pending) return;
const text = Array.from(selectedOptions).join(', ');
setAnswered(true);
setAnsweredText(text);
onAnswer(text);
};
const handleSubmitOther = () => {
const text = otherText.trim();
if (!text || answered || !pending) return;
setAnswered(true);
setAnsweredText(text);
onAnswer(text);
};
const isDisabled = answered || !pending;
return (
<div className="my-1 space-y-3">
{questions.map((q, qi) => (
<div key={qi} className="rounded-xl border border-duck-teal/20 bg-background/90 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 bg-duck-teal/5 border-b border-duck-teal/10">
<MessageCircleQuestion className="h-4 w-4 text-duck-teal shrink-0" />
<span className="text-xs font-medium text-duck-teal uppercase tracking-wider">{q.header}</span>
</div>
<div className="px-4 py-3 space-y-3">
<p className="text-sm text-duck-dark font-medium">{q.question}</p>
<div className="space-y-1.5">
{q.options.map((opt) => {
const isSelected = answered
? answeredText === opt.label || answeredText.split(', ').includes(opt.label)
: selectedOptions.has(opt.label);
return (
<button
key={opt.label}
onClick={() => handleSelect(q, opt.label)}
disabled={isDisabled}
className={`w-full text-left px-3 py-2 rounded-lg border text-sm transition-colors ${
isSelected
? 'border-duck-teal bg-duck-teal/10 text-duck-dark'
: isDisabled
? 'border-duck-dark/10 bg-duck-dark/5 text-duck-dark/40 cursor-not-allowed'
: 'border-duck-dark/15 hover:border-duck-teal/40 hover:bg-duck-teal/5 text-duck-dark cursor-pointer'
}`}
>
<div className="flex items-center gap-2">
{isSelected && <Check className="h-3.5 w-3.5 text-duck-teal shrink-0" />}
<div>
<span className="font-medium">{opt.label}</span>
{opt.description && <span className="text-duck-dark/50 ml-1.5">&mdash; {opt.description}</span>}
</div>
</div>
</button>
);
})}
</div>
{/* "Other" free-text option */}
{!isDisabled && (
<div className="flex gap-2">
<input
type="text"
value={otherText}
onChange={(ev) => setOtherText(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleSubmitOther();
}
}}
placeholder="Other..."
className="flex-1 px-3 py-1.5 rounded-lg border border-duck-dark/15 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:border-duck-teal/40"
/>
<button
onClick={handleSubmitOther}
disabled={!otherText.trim()}
className="px-3 py-1.5 rounded-lg bg-duck-teal text-white text-sm font-medium disabled:opacity-40 disabled:cursor-not-allowed hover:bg-duck-teal/90 transition-colors cursor-pointer"
>
Send
</button>
</div>
)}
{/* Submit button for multi-select */}
{q.multiple && !isDisabled && (
<button
onClick={handleSubmitMultiple}
disabled={selectedOptions.size === 0}
className="px-4 py-1.5 rounded-lg bg-duck-teal text-white text-sm font-medium disabled:opacity-40 disabled:cursor-not-allowed hover:bg-duck-teal/90 transition-colors cursor-pointer"
>
Submit ({selectedOptions.size} selected)
</button>
)}
{/* Answered indicator */}
{isDisabled && answeredText && (
<div className="flex items-center gap-1.5 text-xs text-duck-teal">
<Check className="h-3 w-3" />
<span>Answered: {answeredText}</span>
</div>
)}
</div>
</div>
))}
</div>
);
};