diff --git a/documentation/docusaurus.config.ts b/documentation/docusaurus.config.ts index 86ad7a4e..e83e4162 100644 --- a/documentation/docusaurus.config.ts +++ b/documentation/docusaurus.config.ts @@ -141,17 +141,30 @@ const config: Config = { position: "left", label: "Tutorials", }, - { - to: "/prompt-library", - position: "left", - label: "Prompts", - }, - { - to: "/extensions", - label: "Extensions", - position: "left", - }, { to: "/blog", label: "Blog", position: "left" }, + { + type: 'dropdown', + label: 'Resources', + position: 'left', + items: [ + { + to: '/extensions', + label: 'Extensions', + }, + { + to: '/recipe-generator', + label: 'Recipe Generator', + }, + { + to: '/prompt-library', + label: 'Prompt Library', + }, + { + href: 'https://block.github.io/goose/install-link-generator/', + label: 'Install Link Generator', + }, + ], + }, { href: "https://discord.gg/block-opensource", diff --git a/documentation/src/css/custom.css b/documentation/src/css/custom.css index 28d8f6ac..d25d59f9 100644 --- a/documentation/src/css/custom.css +++ b/documentation/src/css/custom.css @@ -306,6 +306,25 @@ html[data-theme="light"] .hide-in-light { align-items: center; } +/* Dropdown styles */ +.navbar__link--active { + color: var(--text-prominent); +} + +.dropdown__menu { + background-color: var(--background-app); + border-color: var(--border-subtle); +} + +.dropdown__link { + color: var(--text-standard); +} + +.dropdown__link:hover { + background-color: var(--background-subtle); + color: var(--text-prominent); +} + .iconExternalLink_nPIU { margin-left: 8px !important; } diff --git a/documentation/src/pages/extensions/index.tsx b/documentation/src/pages/extensions/index.tsx index 4818c454..d9649697 100644 --- a/documentation/src/pages/extensions/index.tsx +++ b/documentation/src/pages/extensions/index.tsx @@ -4,6 +4,8 @@ import type { MCPServer } from "@site/src/types/server"; import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers"; import { motion } from "framer-motion"; import Layout from "@theme/Layout"; +import Link from "@docusaurus/Link"; +import { Wand2 } from "lucide-react"; export default function HomePage() { const [servers, setServers] = useState([]); @@ -52,13 +54,21 @@ export default function HomePage() {

-
- setSearchQuery(e.target.value)} - /> +
+
+ setSearchQuery(e.target.value)} + /> +
+ +
+ + Recipe Generator +
+
{error && ( diff --git a/documentation/src/pages/recipe-generator.tsx b/documentation/src/pages/recipe-generator.tsx new file mode 100644 index 00000000..12d73ca1 --- /dev/null +++ b/documentation/src/pages/recipe-generator.tsx @@ -0,0 +1,439 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import Layout from "@theme/Layout"; +import { ArrowLeft, Copy, Check, Plus, X } from "lucide-react"; +import { Button } from "@site/src/components/ui/button"; +import Link from "@docusaurus/Link"; + +export default function RecipeGenerator() { + // State management + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [instructions, setInstructions] = useState(''); + const [activities, setActivities] = useState([]); + const [newActivity, setNewActivity] = useState(''); + const [copied, setCopied] = useState(false); + const [errors, setErrors] = useState({}); + const [outputFormat, setOutputFormat] = useState('url'); // 'url' or 'yaml' + const [authorContact, setAuthorContact] = useState(''); + const [extensionsList, setExtensionsList] = useState([ + { type: 'builtin', name: 'developer', display_name: 'Developer', timeout: 300, bundled: true, enabled: false }, + { type: 'builtin', name: 'googledrive', display_name: 'Google Drive', timeout: 300, bundled: true, enabled: false }, + { type: 'builtin', name: 'computercontroller', display_name: 'Computer Controller', timeout: 300, bundled: true, enabled: false }, + { type: 'builtin', name: 'jetbrains', display_name: 'JetBrains', timeout: 300, bundled: true, enabled: false }, + { type: 'builtin', name: 'memory', display_name: 'Memory', timeout: 300, bundled: true, enabled: false }, + { + type: 'stdio', + name: 'pdf-reader', + cmd: 'uvx', + args: ['mcp-read-pdf@latest'], + envs: {}, + env_keys: [], + timeout: null, + description: "Read and analyze PDF documents", + enabled: false + } + ]); + const [prompt, setPrompt] = useState(''); + + // Add activity handler + const handleAddActivity = useCallback(() => { + if (newActivity.trim()) { + setActivities(prev => [...prev, newActivity.trim()]); + setNewActivity(''); + } + }, [newActivity]); + + // Remove activity handler + const handleRemoveActivity = useCallback((index) => { + setActivities(prev => prev.filter((_, i) => i !== index)); + }, []); + + // Toggle extension handler + const toggleExtension = useCallback((index) => { + setExtensionsList(prev => { + const updated = [...prev]; + updated[index] = { ...updated[index], enabled: !updated[index].enabled }; + return updated; + }); + }, []); + + // Form validation + const validateForm = useCallback(() => { + const newErrors = {}; + + if (!title.trim()) { + newErrors.title = 'Title is required'; + } + if (!description.trim()) { + newErrors.description = 'Description is required'; + } + if (!instructions.trim()) { + newErrors.instructions = 'Instructions are required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, [title, description, instructions]); + + // Generate output with useMemo to prevent re-renders + const recipeOutput = useMemo(() => { + // Only generate if we have the required fields + if (!title.trim() || !description.trim() || !instructions.trim()) { + return ''; + } + + try { + if (outputFormat === 'url') { + const recipeConfig = { + version: "1.0.0", + title, + description, + instructions, + activities: activities.length > 0 ? activities : undefined + }; + + // Filter out undefined values + Object.keys(recipeConfig).forEach(key => { + if (recipeConfig[key] === undefined) { + delete recipeConfig[key]; + } + }); + + // Use window.btoa for browser compatibility + return `goose://recipe?config=${window.btoa(JSON.stringify(recipeConfig))}`; + } else { + // Generate YAML format + const enabledExtensions = extensionsList.filter(ext => ext.enabled); + + let yaml = `version: 1.0.0 +title: ${title} +description: ${description} +instructions: ${instructions} +`; + + if (authorContact) { + yaml += `author: + contact: ${authorContact} +`; + } + + if (enabledExtensions.length > 0) { + yaml += `extensions: +`; + for (const ext of enabledExtensions) { + if (ext.type === 'builtin') { + yaml += `- type: ${ext.type} + name: ${ext.name} + display_name: ${ext.display_name} + timeout: ${ext.timeout} + bundled: ${ext.bundled} +`; + } else if (ext.type === 'stdio') { + yaml += `- type: ${ext.type} + name: ${ext.name} + cmd: ${ext.cmd} + args: + - ${ext.args.join('\n - ')} + envs: {} + env_keys: [] + timeout: ${ext.timeout === null ? 'null' : ext.timeout} + description: ${ext.description} +`; + } + } + } + + if (prompt) { + yaml += `prompt: ${prompt} +`; + } + + return yaml; + } + } catch (error) { + console.error('Error generating recipe output:', error); + return ''; + } + }, [title, description, instructions, activities, outputFormat, authorContact, extensionsList, prompt]); + + // Copy handler + const handleCopy = useCallback(() => { + if (validateForm() && recipeOutput) { + navigator.clipboard.writeText(recipeOutput) + .then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }) + .catch(err => console.error('Failed to copy output:', err)); + } + }, [validateForm, recipeOutput]); + + return ( + +
+ +
+

Recipe Generator

+

+ Create a shareable Goose recipe URL that others can use to launch a session with your predefined settings. +

+
+ +
+

Recipe Details

+ + {/* Format Selection */} +
+ +
+ + +
+
+ +
+ {/* Title */} +
+ + setTitle(e.target.value)} + onBlur={validateForm} + className={`w-full p-3 border rounded-lg bg-bgSubtle text-textStandard ${ + errors.title ? 'border-red-500' : 'border-borderSubtle' + }`} + placeholder="Enter a title for your recipe" + /> + {errors.title &&
{errors.title}
} +
+ + {/* Description */} +
+ + setDescription(e.target.value)} + onBlur={validateForm} + className={`w-full p-3 border rounded-lg bg-bgSubtle text-textStandard ${ + errors.description ? 'border-red-500' : 'border-borderSubtle' + }`} + placeholder="Enter a description for your recipe" + /> + {errors.description &&
{errors.description}
} +
+ + {/* Instructions */} +
+ +