Workspaces layout, automation page
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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-white/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">— {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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user