mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 06:34:26 +01:00
docs: generate goose deep links (#2609)
Co-authored-by: Rizel Scarlett <rizel@squareup.com>
This commit is contained in:
@@ -141,17 +141,30 @@ const config: Config = {
|
|||||||
position: "left",
|
position: "left",
|
||||||
label: "Tutorials",
|
label: "Tutorials",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
to: "/prompt-library",
|
|
||||||
position: "left",
|
|
||||||
label: "Prompts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/extensions",
|
|
||||||
label: "Extensions",
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
{ to: "/blog", label: "Blog", 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",
|
href: "https://discord.gg/block-opensource",
|
||||||
|
|||||||
@@ -306,6 +306,25 @@ html[data-theme="light"] .hide-in-light {
|
|||||||
align-items: center;
|
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 {
|
.iconExternalLink_nPIU {
|
||||||
margin-left: 8px !important;
|
margin-left: 8px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { MCPServer } from "@site/src/types/server";
|
|||||||
import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers";
|
import { fetchMCPServers, searchMCPServers } from "@site/src/utils/mcp-servers";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Layout from "@theme/Layout";
|
import Layout from "@theme/Layout";
|
||||||
|
import Link from "@docusaurus/Link";
|
||||||
|
import { Wand2 } from "lucide-react";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [servers, setServers] = useState<MCPServer[]>([]);
|
const [servers, setServers] = useState<MCPServer[]>([]);
|
||||||
@@ -52,13 +54,21 @@ export default function HomePage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="search-container">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<input
|
<div className="search-container flex-1">
|
||||||
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-3 text-[40px] leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
|
<input
|
||||||
placeholder="Search for extensions"
|
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-3 text-[40px] leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
|
||||||
value={searchQuery}
|
placeholder="Search for extensions"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
value={searchQuery}
|
||||||
/>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link to="/recipe-generator" className="no-underline ml-4">
|
||||||
|
<div className="flex items-center gap-2 bg-bgAppInverse text-textProminentInverse px-4 py-3 rounded-lg hover:bg-opacity-90 transition-all">
|
||||||
|
<Wand2 className="h-5 w-5" />
|
||||||
|
<span>Recipe Generator</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
439
documentation/src/pages/recipe-generator.tsx
Normal file
439
documentation/src/pages/recipe-generator.tsx
Normal file
@@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<div className="container mx-auto px-4 py-12 max-w-4xl">
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-4xl font-bold mb-4 text-textProminent">Recipe Generator</h1>
|
||||||
|
<p className="text-lg text-textSubtle">
|
||||||
|
Create a shareable Goose recipe URL that others can use to launch a session with your predefined settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-bgApp border border-borderSubtle rounded-lg p-6 mb-8 shadow-sm">
|
||||||
|
<h2 className="text-2xl font-medium mb-6 text-textProminent">Recipe Details</h2>
|
||||||
|
|
||||||
|
{/* Format Selection */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Output Format
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="format"
|
||||||
|
value="url"
|
||||||
|
checked={outputFormat === 'url'}
|
||||||
|
onChange={() => setOutputFormat('url')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<span className="text-textStandard">URL Format</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="format"
|
||||||
|
value="yaml"
|
||||||
|
checked={outputFormat === 'yaml'}
|
||||||
|
onChange={() => setOutputFormat('yaml')}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<span className="text-textStandard">YAML Format</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Title */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="title" className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Title <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => 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 && <div className="text-red-500 text-sm mt-1">{errors.title}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Description <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => 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 && <div className="text-red-500 text-sm mt-1">{errors.description}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="instructions" className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Instructions <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="instructions"
|
||||||
|
value={instructions}
|
||||||
|
onChange={(e) => setInstructions(e.target.value)}
|
||||||
|
onBlur={validateForm}
|
||||||
|
className={`w-full p-3 border rounded-lg bg-bgSubtle text-textStandard min-h-[150px] ${
|
||||||
|
errors.instructions ? 'border-red-500' : 'border-borderSubtle'
|
||||||
|
}`}
|
||||||
|
placeholder="Enter instructions for the AI (these will be added to the system prompt)"
|
||||||
|
/>
|
||||||
|
{errors.instructions && <div className="text-red-500 text-sm mt-1">{errors.instructions}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* YAML-specific fields */}
|
||||||
|
{outputFormat === 'yaml' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="authorContact" className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Author Contact (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="authorContact"
|
||||||
|
value={authorContact}
|
||||||
|
onChange={(e) => setAuthorContact(e.target.value)}
|
||||||
|
className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard"
|
||||||
|
placeholder="Enter author contact information"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="prompt" className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Initial Prompt (optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="prompt"
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
className="w-full p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard min-h-[100px]"
|
||||||
|
placeholder="Enter an initial prompt for the recipe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Extensions (optional)
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{extensionsList.map((extension, index) => (
|
||||||
|
<div key={index} className="flex items-center p-3 border border-borderSubtle rounded-lg bg-bgSubtle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={`extension-${index}`}
|
||||||
|
checked={extension.enabled}
|
||||||
|
onChange={() => toggleExtension(index)}
|
||||||
|
className="mr-3"
|
||||||
|
/>
|
||||||
|
<label htmlFor={`extension-${index}`} className="flex-1 text-textStandard">
|
||||||
|
<span className="font-medium">{extension.display_name || extension.name}</span>
|
||||||
|
{extension.description && (
|
||||||
|
<span className="block text-sm text-textSubtle">{extension.description}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activities */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-textStandard mb-2">
|
||||||
|
Activities (optional)
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
{activities.map((activity, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="inline-flex items-center bg-bgSubtle border border-borderSubtle rounded-full px-4 py-2 text-sm text-textStandard"
|
||||||
|
>
|
||||||
|
<span>{activity}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveActivity(index)}
|
||||||
|
className="ml-2 text-textSubtle hover:text-red-500 transition-colors bg-transparent border-none"
|
||||||
|
aria-label="Remove activity"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newActivity}
|
||||||
|
onChange={(e) => setNewActivity(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddActivity();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 p-3 border border-borderSubtle rounded-lg bg-bgSubtle text-textStandard"
|
||||||
|
placeholder="Enter an activity"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddActivity}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
disabled={!newActivity.trim()}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Generated Output */}
|
||||||
|
<div className="bg-bgApp border border-borderSubtle rounded-lg p-6 shadow-sm">
|
||||||
|
<h2 className="text-2xl font-medium mb-4 text-textProminent">
|
||||||
|
Generated Recipe {outputFormat === 'url' ? 'URL' : 'YAML'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-bgSubtle rounded-lg p-4 mb-4 overflow-x-auto">
|
||||||
|
<pre className="text-sm text-textStandard font-mono break-all whitespace-pre-wrap">
|
||||||
|
{recipeOutput || `Fill in the required fields to generate a ${outputFormat === 'url' ? 'URL' : 'YAML'}`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
disabled={!recipeOutput}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Copied!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
Copy {outputFormat === 'url' ? 'URL' : 'YAML'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions for Use */}
|
||||||
|
<div className="mt-8 bg-bgApp border border-borderSubtle rounded-lg p-6 shadow-sm">
|
||||||
|
<h2 className="text-2xl font-medium mb-4 text-textProminent">How to Use</h2>
|
||||||
|
<ol className="list-decimal pl-6 space-y-2 text-textStandard">
|
||||||
|
<li>Fill in the required fields above to generate a recipe.</li>
|
||||||
|
<li>Choose between URL format (for direct sharing) or YAML format (for configuration files).</li>
|
||||||
|
<li>For URL format:
|
||||||
|
<ul className="list-disc pl-6 mt-2">
|
||||||
|
<li>Copy the generated URL using the "Copy URL" button.</li>
|
||||||
|
<li>Share the URL with others who have Goose Desktop installed.</li>
|
||||||
|
<li>When someone clicks the URL, it will open Goose Desktop with your recipe configuration.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>For YAML format:
|
||||||
|
<ul className="list-disc pl-6 mt-2">
|
||||||
|
<li>Copy the generated YAML using the "Copy YAML" button.</li>
|
||||||
|
<li>Save it as a <code>.yaml</code> file.</li>
|
||||||
|
<li>Use with the CLI: <code>goose run --recipe your-recipe.yaml</code></li>
|
||||||
|
<li>Or create a deeplink with: <code>goose recipe deeplink your-recipe.yaml</code></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user