import { useState, useRef, useEffect } from 'react';
import {
Stack,
Paper,
Text,
TextInput,
Textarea,
Button,
Group,
Card,
Badge,
Radio,
Loader,
ScrollArea,
Box,
ThemeIcon,
Divider,
Select,
} from '@mantine/core';
import * as TablerIcons from '@tabler/icons-react';
const IconRobot = TablerIcons.IconRobot as React.FC<{ size?: number }>;
const IconUser = TablerIcons.IconUser as React.FC<{ size?: number }>;
const IconSend = TablerIcons.IconSend as React.FC<{ size?: number }>;
const IconEdit = TablerIcons.IconEdit as React.FC<{ size?: number }>;
const IconCheck = TablerIcons.IconCheck as React.FC<{ size?: number }>;
/**
* Input option for choice type
*/
export interface InputOption {
label: string;
value: string;
description?: string;
}
/**
* Input field for form type
*/
export interface InputField {
name: string;
label: string;
type: 'text' | 'number' | 'email' | 'textarea';
required: boolean;
placeholder?: string;
}
/**
* Configuration for input requests
*/
export interface InputConfig {
placeholder?: string;
multiline?: boolean;
options?: InputOption[];
fields?: InputField[];
}
/**
* A message in the interview conversation
*/
export interface InterviewMessage {
id: string;
role: 'assistant' | 'user';
content: string;
timestamp: number;
}
/**
* Pending input request
*/
export interface PendingRequest {
requestId: string;
inputType: 'text' | 'choice' | 'form';
prompt: string;
config?: InputConfig;
}
/**
* Generated report from interview
*/
export interface GeneratedReport {
title: string;
description: string;
severity?: 'low' | 'medium' | 'high' | 'critical';
type?: 'feature_request' | 'change_request' | 'general';
priority?: 'nice_to_have' | 'important' | 'critical';
featureArea?: string;
reproductionSteps?: string[];
}
/**
* Props for InterviewChat component
*/
export interface InterviewChatProps {
/** Messages in the conversation */
messages: InterviewMessage[];
/** Whether we're waiting for AI response */
isThinking: boolean;
/** Whether we're waiting for user input */
waitingForInput: boolean;
/** Current pending input request */
pendingRequest?: PendingRequest;
/** Whether the interview is complete */
isComplete: boolean;
/** Generated report if complete */
generatedReport?: GeneratedReport;
/** Report type */
reportType: 'bug' | 'feedback';
/** Callback when user submits input */
onSubmitInput: (response: string) => void;
/** Callback when user confirms the generated report */
onConfirmReport: (report: GeneratedReport) => void;
/** Callback when user wants to edit the report */
onEditReport?: () => void;
/** Whether submission is in progress */
isSubmitting?: boolean;
}
/**
* Message bubble component
*/
function MessageBubble({ message }: { message: InterviewMessage }) {
const isAssistant = message.role === 'assistant';
return (
{isAssistant && (
)}
{message.content}
{!isAssistant && (
)}
);
}
/**
* Text input form
*/
function TextInputForm({
config,
onSubmit,
isSubmitting,
}: {
config?: InputConfig;
onSubmit: (value: string) => void;
isSubmitting: boolean;
}) {
const [value, setValue] = useState('');
const handleSubmit = () => {
if (value.trim()) {
onSubmit(value.trim());
setValue('');
}
};
if (config?.multiline) {
return (
);
}
return (
setValue(e.target.value)}
style={{ flex: 1 }}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSubmit();
}
}}
/>
}
>
Send
);
}
/**
* Choice input form (radio buttons)
*/
function ChoiceInputForm({
config,
onSubmit,
isSubmitting,
}: {
config?: InputConfig;
onSubmit: (value: string) => void;
isSubmitting: boolean;
}) {
const [selected, setSelected] = useState('');
const handleSubmit = () => {
if (selected) {
onSubmit(selected);
setSelected('');
}
};
if (!config?.options || config.options.length === 0) {
return No options provided;
}
return (
{config.options.map((option) => (
{option.label}
{option.description && (
{option.description}
)}
}
/>
))}
}
>
Send
);
}
/**
* Form input (multiple fields)
*/
function FormInputForm({
config,
onSubmit,
isSubmitting,
}: {
config?: InputConfig;
onSubmit: (value: string) => void;
isSubmitting: boolean;
}) {
const [values, setValues] = useState>({});
const handleSubmit = () => {
const result = JSON.stringify(values);
onSubmit(result);
setValues({});
};
if (!config?.fields || config.fields.length === 0) {
return No fields provided;
}
const isValid = config.fields
.filter((f) => f.required)
.every((f) => values[f.name]?.trim());
return (
{config.fields.map((field) => {
if (field.type === 'textarea') {
return (
);
}
/**
* Report preview component
*/
function ReportPreview({
report,
reportType,
onConfirm,
onEdit,
isSubmitting,
}: {
report: GeneratedReport;
reportType: 'bug' | 'feedback';
onConfirm: (report: GeneratedReport) => void;
onEdit?: () => void;
isSubmitting: boolean;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editedReport, setEditedReport] = useState(report);
const handleConfirm = () => {
onConfirm(isEditing ? editedReport : report);
};
if (isEditing) {
return (
Edit Your {reportType === 'bug' ? 'Bug Report' : 'Feedback'}
setEditedReport({ ...editedReport, title: e.target.value })}
/>
}
>
Submit {reportType === 'bug' ? 'Bug Report' : 'Feedback'}
);
}
return (
Generated {reportType === 'bug' ? 'Bug Report' : 'Feedback'}
Ready to Submit
Title
{report.title}
Description
{report.description}
{reportType === 'bug' && report.severity && (
Severity
{report.severity}
)}
{reportType === 'feedback' && report.type && (
Type
{report.type.replace('_', ' ')}
)}
{reportType === 'feedback' && report.priority && (
Priority
{report.priority.replace('_', ' ')}
)}
{report.featureArea && (
Feature Area
{report.featureArea}
)}
{report.reproductionSteps && report.reproductionSteps.length > 0 && (
Reproduction Steps
{report.reproductionSteps.map((step, i) => (
{i + 1}. {step}
))}
)}
}
onClick={() => setIsEditing(true)}
disabled={isSubmitting}
>
Edit
}
>
Submit {reportType === 'bug' ? 'Bug Report' : 'Feedback'}
);
}
/**
* Main InterviewChat component
*/
export function InterviewChat({
messages,
isThinking,
waitingForInput,
pendingRequest,
isComplete,
generatedReport,
reportType,
onSubmitInput,
onConfirmReport,
onEditReport,
isSubmitting = false,
}: InterviewChatProps) {
const scrollRef = useRef(null);
// Auto-scroll to bottom when messages change
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
}
}, [messages, isThinking, waitingForInput, isComplete]);
return (
{/* Messages area */}
{messages.map((message) => (
))}
{/* Thinking indicator */}
{isThinking && (
Thinking...
)}
{/* Input area */}
{isComplete && generatedReport ? (
) : waitingForInput && pendingRequest ? (
{pendingRequest.prompt}
{pendingRequest.inputType === 'text' && (
)}
{pendingRequest.inputType === 'choice' && (
)}
{pendingRequest.inputType === 'form' && (
)}
) : isThinking ? (
AI is preparing the next question...
) : null}
);
}