Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: form input #465

Open
wants to merge 5 commits into
base: v1.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions ui/desktop/src/components/GooseResponseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import ReactMarkdown from 'react-markdown';
import { Button } from './ui/button';
import { cn } from '../utils';

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;
Expand All @@ -12,11 +26,13 @@ interface GooseResponseFormProps {

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 = [];
let options: Array<{ optionTitle: string; optionDescription: string }> = [];
let dynamicForm: DynamicForm | null = null;

if (metadata) {
window.electron.logInfo('metadata:'+ JSON.stringify(metadata, null, 2));
Expand All @@ -27,6 +43,16 @@ export default function GooseResponseForm({ message: _message, metadata, append
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];
Expand Down Expand Up @@ -92,6 +118,24 @@ export default function GooseResponseForm({ message: _message, metadata, append
}
};

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;
}
Expand All @@ -110,7 +154,7 @@ export default function GooseResponseForm({ message: _message, metadata, append
</Button>
</div>
)}
{isQuestion && isOptions && options.length > 0 && (
{isQuestion && isOptions && Array.isArray(options) && options.length > 0 && (
<div className="space-y-4">
{options.map((opt, index) => (
<div
Expand Down Expand Up @@ -140,6 +184,56 @@ export default function GooseResponseForm({ message: _message, metadata, append
</Button>
</div>
)}
{dynamicForm && dynamicForm.fields && !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-bold 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"
>
<GPSIcon size={14} />
Submit Form
</Button>
</form>
)}
</div>
);
}
83 changes: 79 additions & 4 deletions ui/desktop/src/utils/askAI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { getApiUrl, getSecretKey } from '../config';
const getQuestionClassifierPrompt = (messageContent: string): string => `
You are a simple classifier that takes content and decides if it is asking for input
from a person before continuing if there is more to do, or not. These are questions
on if a course of action should proceeed or not, or approval is needed. If it is a
question asking if it ok to proceed or make a choice, clearly, return QUESTION, otherwise READY if not 97% sure.
on if a course of action should proceeed or not, or approval is needed. If it is CLEARLY a
question asking if it ok to proceed or make a choice or some input is required to proceed, then, and ONLY THEN, return QUESTION, otherwise READY if not 97% sure.

### Examples message content that is classified as READY:
anything else I can do?
Expand All @@ -21,11 +21,13 @@ Would you like any further information or assistance?
Would you like to me to make any changes?
Would you like me to make any adjustments to this implementation?
Would you like me to show you how to…
What would you like to do next?

### Examples that are QUESTIONS:
Should I go ahead and make the changes?
Should I Go ahead with this plan?
Should I focus on X or Y?
Provide me with the name of the package and version you would like to install.


### Message Content:
Expand Down Expand Up @@ -73,6 +75,78 @@ words, phrases, or explanations are allowed.

Response:`;

const getFormPrompt = (messageContent: string): string => `
When you see a request for several pieces of information, then provide a well formed JSON object like will be shown below.
The response will have:
* a title, description,
* a list of fields, each field will have a label, type, name, placeholder, and required (boolean).
(type is either text or textarea only).
If it is not requesting clearly several pieces of information, just return and empty object.
michaelneale marked this conversation as resolved.
Show resolved Hide resolved

### Example Message:
I'll help you scaffold out a Python package. To create a well-structured Python package, I'll need to know a few key pieces of information:

Package name - What would you like to call your package? (This should be a valid Python package name - lowercase, no spaces, typically using underscores for separators if needed)

Brief description - What is the main purpose of the package? This helps in setting up appropriate documentation and structure.

Initial modules - Do you have specific functionality in mind that should be split into different modules?

Python version - Which Python version(s) do you want to support?

Dependencies - Are there any known external packages you'll need?

### Example JSON Response:
{
"title": "Python Package Scaffolding Form",
"description": "Provide the details below to scaffold a well-structured Python package.",
"fields": [
{
"label": "Package Name",
"type": "text",
"name": "package_name",
"placeholder": "Enter the package name (lowercase, no spaces, use underscores if needed)",
"required": true
},
{
"label": "Brief Description",
"type": "textarea",
"name": "brief_description",
"placeholder": "Enter a brief description of the package's purpose",
"required": true
},
{
"label": "Initial Modules",
"type": "textarea",
"name": "initial_modules",
"placeholder": "List the specific functionalities or modules (optional)",
"required": false
},
{
"label": "Python Version(s)",
"type": "text",
"name": "python_versions",
"placeholder": "Enter the Python version(s) to support (e.g., 3.8, 3.9, 3.10)",
"required": true
},
{
"label": "Dependencies",
"type": "textarea",
"name": "dependencies",
"placeholder": "List any known external packages you'll need (optional)",
"required": false
}
]
}

### Message Content:
${messageContent}

You must provide a response strictly as json in the format described. No other
words, phrases, or explanations are allowed.

Response:`;

/**
* Core function to ask the AI a single question and get a response
* @param prompt The prompt to send to the AI
Expand Down Expand Up @@ -107,14 +181,15 @@ export async function askAi(messageContent: string): Promise<string[]> {

// If READY, return early with empty responses for options
if (questionClassification === 'READY') {
return [questionClassification, 'NO', '[]'];
return [questionClassification, 'NO', '[]', '{}'];
}

// Otherwise, proceed with all classifiers in parallel
const prompts = [
Promise.resolve(questionClassification), // Reuse the result we already have
ask(getOptionsClassifierPrompt(messageContent)),
ask(getOptionsFormatterPrompt(messageContent))
ask(getOptionsFormatterPrompt(messageContent)),
ask(getFormPrompt(messageContent)),
];

return Promise.all(prompts);
Expand Down
Loading