mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-25 17:24:25 +01:00
251 lines
7.9 KiB
TypeScript
251 lines
7.9 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import MarkdownContent from './MarkdownContent';
|
|
import { Button } from './ui/button';
|
|
import { cn } from '../utils';
|
|
import { Send } from './icons';
|
|
|
|
interface FormField {
|
|
label: string;
|
|
type: 'text' | 'textarea';
|
|
name: string;
|
|
placeholder: string;
|
|
required: boolean;
|
|
}
|
|
|
|
interface DynamicForm {
|
|
title: string;
|
|
description: string;
|
|
fields: FormField[];
|
|
}
|
|
|
|
interface GooseResponseFormProps {
|
|
message: string;
|
|
metadata: any;
|
|
append: (value: any) => void;
|
|
}
|
|
|
|
export default function GooseResponseForm({
|
|
message: _message,
|
|
metadata,
|
|
append,
|
|
}: GooseResponseFormProps) {
|
|
const [selectedOption, setSelectedOption] = useState<number | null>(null);
|
|
const [formValues, setFormValues] = useState<Record<string, string>>({});
|
|
const prevStatusRef = useRef<string | null>(null);
|
|
|
|
let isQuestion = false;
|
|
let isOptions = false;
|
|
let options: Array<{ optionTitle: string; optionDescription: string }> = [];
|
|
let dynamicForm: DynamicForm | null = null;
|
|
|
|
if (metadata) {
|
|
window.electron.logInfo('metadata:' + JSON.stringify(metadata, null, 2));
|
|
}
|
|
|
|
// Process metadata outside of conditional
|
|
const currentStatus = metadata?.[0] ?? null;
|
|
isQuestion = currentStatus === 'QUESTION';
|
|
isOptions = metadata?.[1] === 'OPTIONS';
|
|
|
|
// Parse dynamic form data if it exists in metadata[3]
|
|
if (metadata?.[3]) {
|
|
try {
|
|
dynamicForm = JSON.parse(metadata[3]);
|
|
} catch (err) {
|
|
console.error('Failed to parse form data:', err);
|
|
dynamicForm = null;
|
|
}
|
|
}
|
|
|
|
if (isQuestion && isOptions && metadata?.[2]) {
|
|
try {
|
|
let optionsData = metadata[2];
|
|
// Use a regular expression to extract the JSON block
|
|
const jsonBlockMatch = optionsData.match(/```json([\s\S]*?)```/);
|
|
|
|
// If a JSON block is found, extract and clean it
|
|
if (jsonBlockMatch) {
|
|
optionsData = jsonBlockMatch[1].trim(); // Extract the content inside the block
|
|
} else {
|
|
// Optionally, handle the case where there is no explicit ```json block
|
|
console.warn('No JSON block found in the provided string.');
|
|
}
|
|
options = JSON.parse(optionsData);
|
|
options = options.filter(
|
|
(opt) => typeof opt.optionTitle === 'string' && typeof opt.optionDescription === 'string'
|
|
);
|
|
} catch (err) {
|
|
console.error('Failed to parse options data:', err);
|
|
options = [];
|
|
}
|
|
}
|
|
|
|
// Move useEffect to top level
|
|
useEffect(() => {
|
|
const currentMetadataStatus = metadata?.[0];
|
|
const shouldNotify =
|
|
currentMetadataStatus &&
|
|
(currentMetadataStatus === 'QUESTION' || currentMetadataStatus === 'OPTIONS') &&
|
|
prevStatusRef.current !== currentMetadataStatus;
|
|
|
|
if (shouldNotify) {
|
|
window.electron.showNotification({
|
|
title: 'Goose has a question for you',
|
|
body: `Please check with Goose to approve the plan of action`,
|
|
});
|
|
}
|
|
|
|
prevStatusRef.current = currentMetadataStatus ?? null;
|
|
}, [metadata]);
|
|
|
|
const handleOptionClick = (index: number) => {
|
|
setSelectedOption(index);
|
|
};
|
|
|
|
const handleAccept = () => {
|
|
const message = {
|
|
content: 'Yes - go ahead.',
|
|
role: 'user',
|
|
};
|
|
append(message);
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (selectedOption !== null && options[selectedOption]) {
|
|
const message = {
|
|
content: `Yes - continue with: ${options[selectedOption].optionTitle}`,
|
|
role: 'user',
|
|
};
|
|
append(message);
|
|
}
|
|
};
|
|
|
|
const handleFormSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (dynamicForm) {
|
|
const message = {
|
|
content: JSON.stringify(formValues),
|
|
role: 'user',
|
|
};
|
|
append(message);
|
|
}
|
|
};
|
|
|
|
const handleFormChange = (name: string, value: string) => {
|
|
setFormValues((prev) => ({
|
|
...prev,
|
|
[name]: value,
|
|
}));
|
|
};
|
|
|
|
if (!metadata) {
|
|
return null;
|
|
}
|
|
|
|
function isForm(f: DynamicForm) {
|
|
return (
|
|
f && f.title && f.description && f.fields && Array.isArray(f.fields) && f.fields.length > 0
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{isQuestion && !isOptions && !isForm(dynamicForm) && (
|
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-tool-card dark:bg-tool-card-dark border dark:border-dark-border">
|
|
<Button
|
|
onClick={handleAccept}
|
|
variant="default"
|
|
className="w-full sm:w-auto dark:bg-button-dark"
|
|
>
|
|
<Send className="h-[14px] w-[14px]" />
|
|
Take flight with this plan
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isQuestion && isOptions && Array.isArray(options) && options.length > 0 && (
|
|
<div className="space-y-4">
|
|
{options.map((opt, index) => (
|
|
<div
|
|
key={index}
|
|
onClick={() => handleOptionClick(index)}
|
|
className={cn(
|
|
'p-4 rounded-lg border transition-colors cursor-pointer',
|
|
selectedOption === index
|
|
? 'bg-primary/10 dark:bg-dark-primary border-primary dark:border-dark-primary'
|
|
: 'bg-tool-card dark:bg-tool-card-dark hover:bg-accent dark:hover:bg-dark-accent'
|
|
)}
|
|
>
|
|
<h3 className="font-semibold text-lg mb-2 dark:text-gray-100">{opt.optionTitle}</h3>
|
|
<div className="prose prose-xs max-w-none dark:text-gray-100">
|
|
<MarkdownContent content={opt.optionDescription} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
<Button
|
|
onClick={handleSubmit}
|
|
variant="default"
|
|
className="w-full sm:w-auto dark:bg-button-dark"
|
|
disabled={selectedOption === null}
|
|
>
|
|
<Send className="h-[14px] w-[14px]" />
|
|
Select plan
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isForm(dynamicForm) && !isOptions && (
|
|
<form
|
|
onSubmit={handleFormSubmit}
|
|
className="space-y-4 p-4 rounded-lg bg-tool-card dark:bg-tool-card-dark border dark:border-dark-border"
|
|
>
|
|
<h2 className="text-xl font-medium mb-2 dark:text-gray-100">{dynamicForm.title}</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">{dynamicForm.description}</p>
|
|
|
|
{dynamicForm.fields.map((field) => (
|
|
<div key={field.name} className="space-y-2">
|
|
<label
|
|
htmlFor={field.name}
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-200"
|
|
>
|
|
{field.label}
|
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
{field.type === 'textarea' ? (
|
|
<textarea
|
|
id={field.name}
|
|
name={field.name}
|
|
placeholder={field.placeholder}
|
|
required={field.required}
|
|
value={formValues[field.name] || ''}
|
|
onChange={(e) => handleFormChange(field.name, e.target.value)}
|
|
className="w-full p-2 border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
|
rows={4}
|
|
/>
|
|
) : (
|
|
<input
|
|
type="text"
|
|
id={field.name}
|
|
name={field.name}
|
|
placeholder={field.placeholder}
|
|
required={field.required}
|
|
value={formValues[field.name] || ''}
|
|
onChange={(e) => handleFormChange(field.name, e.target.value)}
|
|
className="w-full p-2 border rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="default"
|
|
className="w-full sm:w-auto mt-4 dark:bg-button-dark"
|
|
>
|
|
<Send className="h-[14px] w-[14px]" />
|
|
Submit Form
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|