mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 06:34:26 +01:00
Docs: Add Goose Recipes Cookbook Page (#2998)
This commit is contained in:
30
.github/ISSUE_TEMPLATE/reply-to-recipe.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/reply-to-recipe.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Auto-reply to Recipe Submissions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
thank-you-comment:
|
||||
if: contains(github.event.issue.title, '[Recipe]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add thank-you comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const commentBody = [
|
||||
"🎉 Thanks for submitting your Goose recipe to the Cookbook!",
|
||||
"",
|
||||
"We appreciate you sharing your workflow with the community — our team will review your submission soon.",
|
||||
"If accepted, it’ll be added to the [Goose Recipes Cookbook](https://block.github.io/goose/recipes) and you’ll receive LLM credits as a thank-you!",
|
||||
"",
|
||||
"Stay tuned — and keep those recipes coming 🧑🍳🔥"
|
||||
].join('\n');
|
||||
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: commentBody
|
||||
});
|
||||
51
.github/ISSUE_TEMPLATE/submit-recipe.yml
vendored
Normal file
51
.github/ISSUE_TEMPLATE/submit-recipe.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: 🧑🍳 Submit a Recipe to the Goose Cookbook
|
||||
description: Share a reusable Goose session (aka a recipe) to help the community!
|
||||
title: "[Recipe] <your recipe title here>"
|
||||
labels: ["recipe submission"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for contributing to the Goose Cookbook! 🍳
|
||||
Recipes are reusable sessions created in Goose Desktop or CLI and shared with the community to help others vibe-code faster.
|
||||
|
||||
📌 **How to Submit**
|
||||
- Create your recipe using Goose (“Make Agent from this session”)
|
||||
- Fill out the JSON below using the format provided
|
||||
- Paste it into the field and submit the issue — we’ll review and add it to the Cookbook!
|
||||
|
||||
🪄 **What Happens After?**
|
||||
- If accepted, we’ll publish your recipe to the [Goose Recipes Cookbook](https://block.github.io/goose/recipes)
|
||||
- You’ll receive LLM credits as a thank-you!
|
||||
- Your GitHub handle will be displayed and linked on the recipe card
|
||||
|
||||
- type: textarea
|
||||
id: recipe-json
|
||||
attributes:
|
||||
label: Paste Your Recipe JSON Below
|
||||
description: Use the structure below and be sure to include your GitHub handle.
|
||||
placeholder: |
|
||||
{
|
||||
"id": "pr-generator",
|
||||
"title": "PR Generator",
|
||||
"description": "Generate pull request descriptions based on staged changes and git history.",
|
||||
"action": "Generate PR",
|
||||
"category": "Developer",
|
||||
"extensions": ["GitHub MCP", "Memory"],
|
||||
"activities": ["Summarize changes", "Create PR branch", "Push PR"],
|
||||
"recipeUrl": "https://goose.block.xyz/recipe/pr-generator",
|
||||
"author": "your-github-handle"
|
||||
}
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
🛠 **Recipe Field Tips**
|
||||
- `"id"` should be lowercase, hyphenated, and unique (e.g. `my-awesome-recipe`)
|
||||
- `"action"` describes the core purpose of the recipe (e.g., `Generate Docs`)
|
||||
- `"category"` is the type of user this recipe is for (e.g., `Developer`, `Entertainment`)
|
||||
- `"extensions"` are the Goose tools this recipe uses
|
||||
- `"recipeUrl"` is from Goose Desktop/CLI
|
||||
- `"author"` is your GitHub handle — we’ll link to your profile in the Cookbook
|
||||
@@ -159,6 +159,10 @@ const config: Config = {
|
||||
to: '/prompt-library',
|
||||
label: 'Prompt Library',
|
||||
},
|
||||
{
|
||||
to: '/recipes',
|
||||
label: 'Recipe Cookbook',
|
||||
},
|
||||
{
|
||||
to: 'deeplink-generator',
|
||||
label: 'Deeplink Generator',
|
||||
|
||||
109
documentation/src/components/recipe-card.tsx
Normal file
109
documentation/src/components/recipe-card.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
import Link from "@docusaurus/Link";
|
||||
|
||||
export type Recipe = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
extensions: string[];
|
||||
activities: string[];
|
||||
recipeUrl: string;
|
||||
action?: string;
|
||||
author?: string;
|
||||
persona?: string;
|
||||
};
|
||||
|
||||
export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||
return (
|
||||
<Link
|
||||
to={`/recipes/detail?id=${recipe.id}`}
|
||||
className="block no-underline hover:no-underline h-full"
|
||||
>
|
||||
<div className="relative w-full h-full">
|
||||
{/* Optional Glow */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-purple-500 opacity-10 blur-2xl" />
|
||||
|
||||
{/* Card Container */}
|
||||
<div className="relative z-10 w-full h-full rounded-2xl border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-[#1A1A1A] flex flex-col justify-between p-6 transition-shadow duration-200 ease-in-out hover:shadow-[0_0_0_2px_rgba(99,102,241,0.4),_0_4px_20px_rgba(99,102,241,0.1)]">
|
||||
<div className="space-y-4">
|
||||
{/* Title & Description */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-base text-zinc-900 dark:text-white leading-snug">
|
||||
{recipe.title}
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
|
||||
{recipe.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Extensions */}
|
||||
{recipe.extensions.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{recipe.extensions.map((ext, index) => {
|
||||
const cleanedLabel = ext.replace(/MCP/i, "").trim();
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center h-7 px-3 rounded-full
|
||||
border border-zinc-300 bg-zinc-100 text-zinc-700
|
||||
dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300
|
||||
text-xs font-medium"
|
||||
>
|
||||
{cleanedLabel}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activities */}
|
||||
{recipe.activities?.length > 0 && (
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-700 pt-2 mt-2 flex flex-wrap gap-2">
|
||||
{recipe.activities.map((activity, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center h-7 px-3 rounded-full
|
||||
border border-zinc-300 bg-zinc-100 text-zinc-700
|
||||
dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300
|
||||
text-xs font-medium"
|
||||
>
|
||||
{activity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-between items-center pt-6 mt-2">
|
||||
<a
|
||||
href={recipe.recipeUrl}
|
||||
className="text-sm font-medium text-purple-600 hover:underline dark:text-purple-400"
|
||||
target="_blank"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Launch Recipe →
|
||||
</a>
|
||||
{recipe.author && (
|
||||
<a
|
||||
href={`https://github.com/${recipe.author}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-zinc-500 hover:underline dark:text-zinc-300"
|
||||
title="Recipe author"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<img
|
||||
src={`https://github.com/${recipe.author}.png`}
|
||||
alt={recipe.author}
|
||||
className="w-5 h-5 rounded-full"
|
||||
/>
|
||||
@{recipe.author}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "joke-of-the-day",
|
||||
"title": "Joke of the day",
|
||||
"description": "Will tell you a joke of the day based on the current day",
|
||||
"instructions": "Your job is to tell a joke of the day",
|
||||
"author": "DOsinga",
|
||||
"extensions": ["Developer"],
|
||||
"activities": ["Tell a joke", "Daily humor"],
|
||||
"category": "Entertainment",
|
||||
"recipeUrl": "goose://recipe?config=eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aXRsZSI6Ikpva2Ugb2YgdGhlIGRheSIsImRlc2NyaXB0aW9uIjoiV2lsbCB0ZWxsIHlvdSBhIGpva2Ugb2YgdGhlIGRheSBiYXNlZCBvbiB0aGUgY3VycmVudCBkYXkiLCJpbnN0cnVjdGlvbnMiOiJCYXNlZCBvbiB3aGF0IGRheSBpdCBpcyB0b2RheSwgZ2VuZXJhdGUgYSBqb2tlLiBNZW50aW9uIHRoZSBkYXkgb24gdGhlIGZpcnN0IGxpbmUgdGhlbiBhbiBlbXB0eSBsaW5lIGFuZCB0aGVuIHRoZSBqb2tlLiBEb24ndCBqdXN0IHNheSB0aGUgZGF0ZSwgYnV0IGZpZ3VyZSBvdXQgaWYgdGhlcmUncyBhbnkgY3VsdHVyYWwgc2lnbmlmaWNhbmNlLCBsaWtlIG5hdGlvbmFsIHNoZWx2ZXMgZGF5IiwiZXh0ZW5zaW9ucyI6W10sImFjdGl2aXRpZXMiOlsiR2VuZXJhdGUgaG9saWRheS10aGVtZWQgam9rZSIsIklkZW50aWZ5IHNwZWNpYWwgb2JzZXJ2YW5jZSBmb3IgZGF0ZSIsIkNyZWF0ZSB0aGVtZWQgd29yZHBsYXkiLCJNYXRjaCBodW1vciB0byBjdWx0dXJhbCBldmVudCIsIlRlbGwgYSBqb2tlIl0sImF1dGhvciI6eyJjb250YWN0IjoiZWJvbnlsIn19",
|
||||
"prompt": "Based on what day it is today, generate a joke. Mention the day on the first line then an empty line and then the joke. Don't just say the date, but figure out if there's any cultural significance, like national shelves day"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "pr-generator",
|
||||
"title": "PR Generator",
|
||||
"description": "Automatically generate pull request descriptions based on changes in a local git repo.",
|
||||
"instructions": "Your job is to generate descriptive and helpful pull request descriptions without asking for additional information. Generate commit messages and branch names based on the actual code changes.",
|
||||
"prompt": "Analyze the staged changes and any unpushed commits in the git repository {{git_repo_path}} to generate a comprehensive pull request description. Work autonomously without requesting additional information.\n\nAnalysis steps:\n1. Get current branch name using `git branch --show-current`\n2. If not on main/master/develop:\n - Check for unpushed commits: `git log @{u}..HEAD` (if upstream exists)\n - Include these commits in the analysis\n3. Check staged changes: `git diff --staged`\n4. Save the staged changes diff for the PR description\n5. Determine the type of change (feature, fix, enhancement, etc.) from the code\n\nGenerate the PR description with:\n1. A clear summary of the changes, including:\n - New staged changes\n - Any unpushed commits (if on a feature branch)\n2. Technical implementation details based on both the diff and unpushed commits\n3. List of modified files and their purpose\n4. Impact analysis (what areas of the codebase are affected)\n5. Testing approach and considerations\n6. Any migration steps or breaking changes\n7. Related issues or dependencies\n\nUse git commands:\n- `git diff --staged` for staged changes\n- `git log @{u}..HEAD` for unpushed commits\n- `git branch --show-current` for current branch\n- `git status` for staged files\n- `git show` for specific commit details\n- `git rev-parse --abbrev-ref --symbolic-full-name @{u}` to check if branch has upstream\n\nFormat the description in markdown with appropriate sections and code blocks where relevant.\n\n{% if push_pr %}\nExecute the following steps for pushing:\n1. Determine branch handling:\n - If current branch is main/master/develop or unrelated:\n - Generate branch name from staged changes (e.g., 'feature-add-user-auth')\n - Create and switch to new branch: `git checkout -b [branch-name]`\n - If current branch matches changes:\n - Continue using current branch\n - Note any unpushed commits\n\n2. Handle commits and push:\n a. If staged changes exist:\n - Create commit using generated message: `git commit -m \"[type]: [summary]\"`\n - Message should be concise and descriptive of actual changes\n b. Push changes:\n - For existing branches: `git push origin HEAD`\n - For new branches: `git push -u origin HEAD`\n\n3. Create PR:\n - Use git/gh commands to create PR with generated description\n - Set base branch appropriately\n - Print PR URL after creation\n\nBranch naming convention:\n- Use kebab-case\n- Prefix with type: feature-, fix-, enhance-, refactor-\n- Keep names concise but descriptive\n- Base on actual code changes\n\nCommit message format:\n- Start with type: feat, fix, enhance, refactor\n- Followed by concise description\n- Based on actual code changes\n- No body text needed for straightforward changes\n\nDo not:\n- Ask for confirmation or additional input\n- Create placeholder content\n- Include TODO items\n- Add WIP markers\n{% endif %}",
|
||||
"extensions": ["Developer", "Memory"],
|
||||
"activities": ["Generate PR", "Analyze staged git changes", "Create PR description"],
|
||||
"action": "Generate PR",
|
||||
"category": "Developer",
|
||||
"recipeUrl": "goose://recipe?config=eyJ2ZXJzaW9uIjoiMS4wLjAiLCJ0aXRsZSI6IlB1bGwgUmVxdWVzdCBHZW5lcmF0b3IiLCJkZXNjcmlwdGlvbiI6IkF1dG9tYXRpY2FsbHkgZ2VuZXJhdGUgcHVsbCByZXF1ZXN0IGRlc2NyaXB0aW9ucyBiYXNlZCBvbiBjaGFuZ2VzIGluIGEgbG9jYWwgZ2l0IHJlcG8uIiwiaW5zdHJ1Y3Rpb25zIjoiWW91ciBqb2IgaXMgdG8gZ2VuZXJhdGUgZGVzY3JpcHRpdmUgYW5kIGhlbHBmdWwgcHVsbCByZXF1ZXN0IGRlc2NyaXB0aW9ucyB3aXRob3V0IGFza2luZyBmb3IgYWRkaXRpb25hbCBpbmZvcm1hdGlvbi4gR2VuZXJhdGUgY29tbWl0IG1lc3NhZ2VzIGFuZCBicmFuY2ggbmFtZXMgYmFzZWQgb24gdGhlIGFjdHVhbCBjb2RlIGNoYW5nZXMuIiwiZXh0ZW5zaW9ucyI6W10sImFjdGl2aXRpZXMiOlsiR2VuZXJhdGUgUFIgZGVzY3JpcHRpb24gZnJvbSBjaGFuZ2VzIiwiQW5hbHl6ZSBnaXQgZGlmZiBvdXRwdXQiLCJDcmVhdGUgZmVhdHVyZSBicmFuY2ggYW5kIHB1c2giLCJGb3JtYXQgY29tbWl0IG1lc3NhZ2VzIiwiUmV2aWV3IHVucHVzaGVkIGNvbW1pdHMiXSwiYXV0aG9yIjp7ImNvbnRhY3QiOiJlYm9ueWwifX0=",
|
||||
"author": "lifeizhou-ap"
|
||||
}
|
||||
189
documentation/src/pages/recipes/detail.tsx
Normal file
189
documentation/src/pages/recipes/detail.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import Layout from "@theme/Layout";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useLocation } from "@docusaurus/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "@docusaurus/Link";
|
||||
import Admonition from "@theme/Admonition";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import { Button } from "@site/src/components/ui/button";
|
||||
import { getRecipeById } from "@site/src/utils/recipes";
|
||||
import type { Recipe } from "@site/src/components/recipe-card";
|
||||
|
||||
const colorMap: { [key: string]: string } = {
|
||||
"GitHub MCP": "bg-yellow-100 text-yellow-800 border-yellow-200",
|
||||
"Context7 MCP": "bg-purple-100 text-purple-800 border-purple-200",
|
||||
"Memory": "bg-blue-100 text-blue-800 border-blue-200",
|
||||
};
|
||||
|
||||
export default function RecipeDetailPage(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const [recipe, setRecipe] = useState<Recipe | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecipe = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get("id");
|
||||
if (!id) {
|
||||
setError("No recipe ID provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const recipeData = await getRecipeById(id);
|
||||
if (recipeData) {
|
||||
setRecipe(recipeData);
|
||||
} else {
|
||||
setError("Recipe not found");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load recipe details");
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRecipe();
|
||||
}, [location]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen flex items-start justify-center py-16">
|
||||
<div className="container max-w-5xl mx-auto px-4 animate-pulse">
|
||||
<div className="h-12 w-48 bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-4"></div>
|
||||
<div className="h-6 w-full bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-2"></div>
|
||||
<div className="h-6 w-2/3 bg-bgSubtle dark:bg-zinc-800 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !recipe) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen flex items-start justify-center py-16">
|
||||
<div className="container max-w-5xl mx-auto px-4 text-red-500">
|
||||
{error || "Recipe not found"}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen py-12">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="mb-8 flex justify-between items-start">
|
||||
<Link to="/recipes">
|
||||
<Button className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
</Link>
|
||||
{recipe.author && (
|
||||
<a
|
||||
href={`https://github.com/${recipe.author}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-textSubtle hover:underline"
|
||||
>
|
||||
<img
|
||||
src={`https://github.com/${recipe.author}.png`}
|
||||
alt={recipe.author}
|
||||
className="w-6 h-6 rounded-full"
|
||||
/>
|
||||
@{recipe.author}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-[#1A1A1A] border border-borderSubtle dark:border-zinc-700 rounded-xl p-8 shadow-md">
|
||||
<h1 className="text-4xl font-semibold mb-2 text-textProminent dark:text-white">
|
||||
{recipe.title}
|
||||
</h1>
|
||||
<p className="text-textSubtle dark:text-zinc-400 text-lg mb-6">{recipe.description}</p>
|
||||
|
||||
{/* Activities */}
|
||||
{recipe.activities?.length > 0 && (
|
||||
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-2xl font-medium mb-2 text-textProminent dark:text-white">Activities</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recipe.activities.map((activity, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-surfaceHighlight dark:bg-zinc-900 border border-border dark:border-zinc-700 rounded-full px-3 py-1 text-sm text-textProminent dark:text-zinc-300"
|
||||
>
|
||||
{activity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extensions */}
|
||||
{recipe.extensions?.length > 0 && (
|
||||
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-2xl font-medium mb-2 text-textProminent dark:text-white">Extensions</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recipe.extensions.map((ext, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`border rounded-full px-3 py-1 text-sm ${
|
||||
colorMap[ext] ||
|
||||
"bg-gray-100 text-gray-800 border-gray-200 dark:bg-zinc-900 dark:text-zinc-300 dark:border-zinc-700"
|
||||
}`}
|
||||
>
|
||||
{ext}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
{recipe.instructions && (
|
||||
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-2xl font-medium mb-2 text-textProminent dark:text-white">Instructions</h2>
|
||||
<p className="text-textSubtle dark:text-zinc-400 whitespace-pre-line">
|
||||
{recipe.instructions}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt */}
|
||||
{recipe.prompt && (
|
||||
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-2xl font-medium mb-4 text-textProminent dark:text-white">Initial Prompt</h2>
|
||||
<Admonition type="info" className="mb-4">
|
||||
This prompt auto-starts the recipe when launched in Goose.
|
||||
</Admonition>
|
||||
<CodeBlock language="markdown">{recipe.prompt}</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Launch Button */}
|
||||
{recipe.recipeUrl && (
|
||||
<div className="pt-8 border-t border-borderSubtle dark:border-zinc-700 mt-6">
|
||||
<Link
|
||||
to={recipe.recipeUrl}
|
||||
target="_blank"
|
||||
className="inline-block text-white bg-black dark:bg-white dark:text-black px-6 py-2 rounded-full text-sm font-medium hover:bg-gray-900 dark:hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Launch Recipe →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
217
documentation/src/pages/recipes/index.tsx
Normal file
217
documentation/src/pages/recipes/index.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { RecipeCard, Recipe } from "@site/src/components/recipe-card";
|
||||
import { searchRecipes } from "@site/src/utils/recipes";
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Layout from "@theme/Layout";
|
||||
import Admonition from "@theme/Admonition";
|
||||
import { Button } from "@site/src/components/ui/button";
|
||||
import { SidebarFilter, type SidebarFilterGroup } from "@site/src/components/ui/sidebar-filter";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
export default function RecipePage() {
|
||||
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
|
||||
const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const recipesPerPage = 20;
|
||||
|
||||
const uniqueCategories = Array.from(
|
||||
new Set(recipes.map((r) => r.category?.toLowerCase()).filter(Boolean))
|
||||
).map((category) => ({
|
||||
label: category.replace(/\b\w/g, (l) => l.toUpperCase()),
|
||||
value: category
|
||||
}));
|
||||
|
||||
const uniqueExtensions = Array.from(
|
||||
new Set(recipes.flatMap((r) =>
|
||||
r.extensions.map((ext) => ext.toLowerCase().replace(/\s+/g, "-"))
|
||||
))
|
||||
).map((ext) => {
|
||||
const cleanValue = ext.replace(/-mcp$/, "");
|
||||
let label = cleanValue.replace(/-/g, " ");
|
||||
if (label.toLowerCase() === "github") {
|
||||
label = "GitHub";
|
||||
} else {
|
||||
label = label.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value: ext
|
||||
};
|
||||
});
|
||||
|
||||
const sidebarFilterGroups: SidebarFilterGroup[] = [
|
||||
{
|
||||
title: "Category",
|
||||
options: uniqueCategories
|
||||
},
|
||||
{
|
||||
title: "Extensions Used",
|
||||
options: uniqueExtensions
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecipes = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const results = await searchRecipes(searchQuery);
|
||||
setRecipes(results);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Unknown error";
|
||||
setError(`Failed to load recipes: ${errorMessage}`);
|
||||
console.error("Error loading recipes:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(loadRecipes, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchQuery]);
|
||||
|
||||
let filteredRecipes = recipes;
|
||||
|
||||
Object.entries(selectedFilters).forEach(([group, values]) => {
|
||||
if (values.length > 0) {
|
||||
filteredRecipes = filteredRecipes.filter((r) => {
|
||||
if (group === "Category") {
|
||||
return values.includes(r.category?.toLowerCase());
|
||||
}
|
||||
if (group === "Extensions Used") {
|
||||
return r.extensions?.some((ext) =>
|
||||
values.includes(ext.toLowerCase().replace(/\s+/g, "-"))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container mx-auto px-4 py-8 md:p-24">
|
||||
<div className="pb-8 md:pb-16">
|
||||
<h1 className="text-4xl md:text-[64px] font-medium text-textProminent">
|
||||
Recipes Cookbook
|
||||
</h1>
|
||||
<p className="text-textProminent">
|
||||
Save time and skip setup — launch any{" "}
|
||||
<Link to="/docs/guides/session-recipes" className="text-purple-600 hover:underline">
|
||||
Goose agent recipe
|
||||
</Link>{" "}
|
||||
shared by the community with a single click.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="search-container mb-6 md:mb-8">
|
||||
<input
|
||||
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-2 md:py-3 text-2xl md:text-[40px] leading-tight md:leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
|
||||
placeholder="Search for recipes by keyword"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden mb-4">
|
||||
<Button onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}>
|
||||
{isMobileFilterOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
{isMobileFilterOpen ? "Close Filters" : "Show Filters"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className={`${isMobileFilterOpen ? "block" : "hidden"} md:block md:w-64 mt-6`}>
|
||||
<SidebarFilter
|
||||
groups={sidebarFilterGroups}
|
||||
selectedValues={selectedFilters}
|
||||
onChange={(group, values) => {
|
||||
setSelectedFilters(prev => ({ ...prev, [group]: values }));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className={`${searchQuery ? "pb-2" : "pb-4 md:pb-8"}`}>
|
||||
<p className="text-gray-600">
|
||||
{searchQuery
|
||||
? `${filteredRecipes.length} result${filteredRecipes.length !== 1 ? "s" : ""} for "${searchQuery}"`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Admonition type="danger" title="Error">
|
||||
<p>{error}</p>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-xl text-gray-600">Loading recipes...</div>
|
||||
) : filteredRecipes.length === 0 ? (
|
||||
<Admonition type="info">
|
||||
<p>
|
||||
{searchQuery
|
||||
? "No recipes found matching your search."
|
||||
: "No recipes have been submitted yet."}
|
||||
</p>
|
||||
</Admonition>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{filteredRecipes
|
||||
.slice((currentPage - 1) * recipesPerPage, currentPage * recipesPerPage)
|
||||
.map((recipe) => (
|
||||
<motion.div
|
||||
key={recipe.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<RecipeCard recipe={recipe} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredRecipes.length > recipesPerPage && (
|
||||
<div className="flex justify-center items-center gap-2 md:gap-4 mt-6 md:mt-8">
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<span className="text-textProminent text-sm md:text-base">
|
||||
Page {currentPage} of {Math.ceil(filteredRecipes.length / recipesPerPage)}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(filteredRecipes.length / recipesPerPage), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(filteredRecipes.length / recipesPerPage)}
|
||||
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
179
documentation/src/pages/recipes/styles/main.css
Normal file
179
documentation/src/pages/recipes/styles/main.css
Normal file
@@ -0,0 +1,179 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: Cash Sans;
|
||||
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Regular.woff2)
|
||||
format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Cash Sans;
|
||||
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Medium.woff2)
|
||||
format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Cash Sans;
|
||||
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Semibold.woff2)
|
||||
format("woff2");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Cash Sans;
|
||||
src: url(https://cash-f.squarecdn.com/static/fonts/cashsans/woff2/CashSans-Bold.woff2)
|
||||
format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* start arcade colors */
|
||||
--constant-white: #ffffff;
|
||||
--constant-black: #000000;
|
||||
--grey-10: #101010;
|
||||
--grey-20: #1e1e1e;
|
||||
--grey-50: #666666;
|
||||
--grey-60: #959595;
|
||||
--grey-80: #cccccc;
|
||||
--grey-85: #dadada;
|
||||
--grey-90: #e8e8e8;
|
||||
--grey-95: #f0f0f0;
|
||||
--dark-grey-15: #1a1a1a;
|
||||
--dark-grey-25: #232323;
|
||||
--dark-grey-30: #2a2a2a;
|
||||
--dark-grey-40: #333333;
|
||||
--dark-grey-45: #595959;
|
||||
--dark-grey-60: #878787;
|
||||
--dark-grey-90: #e1e1e1;
|
||||
|
||||
--background-app: var(--constant-white);
|
||||
--background-prominent: var(--grey-80);
|
||||
--background-standard: var(--grey-90);
|
||||
--background-subtle: var(--grey-95);
|
||||
|
||||
--border-divider: var(--grey-90);
|
||||
--border-inverse: var(--constant-white);
|
||||
--border-prominent: var(--grey-10);
|
||||
--border-standard: var(--grey-60);
|
||||
--border-subtle: var(--grey-90);
|
||||
|
||||
--icon-disabled: var(--grey-60);
|
||||
--icon-extra-subtle: var(--grey-60);
|
||||
--icon-inverse: var(--constant-white);
|
||||
--icon-prominent: var(--grey-10);
|
||||
--icon-standard: var(--grey-20);
|
||||
--icon-subtle: var(--grey-50);
|
||||
|
||||
--text-placeholder: var(--grey-60);
|
||||
--text-prominent: var(--grey-10);
|
||||
--text-standard: var(--grey-20);
|
||||
--text-subtle: var(--grey-50);
|
||||
|
||||
&.dark {
|
||||
--background-app: var(--constant-black);
|
||||
--background-prominent: var(--dark-grey-40);
|
||||
--background-standard: var(--dark-grey-25);
|
||||
--background-subtle: var(--dark-grey-15);
|
||||
|
||||
--border-divider: var(--dark-grey-25);
|
||||
--border-inverse: var(--constant-black);
|
||||
--border-prominent: var(--constant-white);
|
||||
--border-standard: var(--dark-grey-45);
|
||||
--border-subtle: var(--dark-grey-25);
|
||||
|
||||
--icon-disabled: var(--dark-grey-45);
|
||||
--icon-extra-subtle: var(--dark-grey-45);
|
||||
--icon-inverse: var(--constant-black);
|
||||
--icon-prominent: var(--constant-white);
|
||||
--icon-standard: var(--dark-grey-90);
|
||||
--icon-subtle: var(--dark-grey-60);
|
||||
|
||||
--text-placeholder: var(--dark-grey-45);
|
||||
--text-prominent: var(--constant-white);
|
||||
--text-standard: var(--dark-grey-90);
|
||||
--text-subtle: var(--dark-grey-60);
|
||||
}
|
||||
/* end arcade colors */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appear-top {
|
||||
0% {
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Command section styles */
|
||||
.command-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-standard);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.command-toggle:hover {
|
||||
color: var(--text-prominent);
|
||||
}
|
||||
|
||||
.command-toggle h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.command-toggle svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.command-content {
|
||||
background-color: var(--background-subtle);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-standard);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.command-content code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* view transitions */
|
||||
a.transitioning .home-page-server-name,
|
||||
.detail-page-server-name {
|
||||
view-transition-name: server-name;
|
||||
}
|
||||
55
documentation/src/pages/recipes/types/index.tsx
Normal file
55
documentation/src/pages/recipes/types/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import Layout from '@docusaurus/theme-classic/lib/theme/Layout';
|
||||
import CodeBlock from '@docusaurus/theme-classic/lib/theme/CodeBlock';
|
||||
|
||||
const Types: React.FC = () => {
|
||||
return (
|
||||
<Layout title="Types" description="Type definitions for the Prompt Library">
|
||||
<div className="container margin-vert--lg">
|
||||
<h1>Type Definitions</h1>
|
||||
<p>This page contains the type definitions used in the Prompt Library.</p>
|
||||
|
||||
<h2>Environment Variable</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type EnvironmentVariable = {
|
||||
name: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
};`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>Extension</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type Extension = {
|
||||
name: string;
|
||||
command: string;
|
||||
is_builtin: boolean;
|
||||
link?: string;
|
||||
installation_notes?: string;
|
||||
environmentVariables?: EnvironmentVariable[];
|
||||
};`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>Category</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type Category = "business" | "technical" | "productivity";`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>Prompt</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type Prompt = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
example_prompt: string;
|
||||
extensions: Extension[];
|
||||
category: Category;
|
||||
featured?: boolean;
|
||||
};`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Types;
|
||||
47
documentation/src/utils/recipes.ts
Normal file
47
documentation/src/utils/recipes.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Recipe } from "@site/src/components/recipe-card";
|
||||
|
||||
// Webpack context loader for all JSON files in the recipes folder
|
||||
const recipeFiles = require.context(
|
||||
'../pages/recipes/data/recipes',
|
||||
false,
|
||||
/\.json$/
|
||||
);
|
||||
|
||||
export function getRecipeById(id: string): Recipe | null {
|
||||
const allRecipes: Recipe[] = recipeFiles
|
||||
.keys()
|
||||
.map((key: string) => recipeFiles(key))
|
||||
.map((module: any) => module.default || module);
|
||||
|
||||
return allRecipes.find((recipe) => recipe.id === id) || null;
|
||||
}
|
||||
|
||||
export async function searchRecipes(query: string): Promise<Recipe[]> {
|
||||
const allRecipes: Recipe[] = recipeFiles
|
||||
.keys()
|
||||
.map((key: string) => recipeFiles(key))
|
||||
.map((module: any) => {
|
||||
const recipe = module.default || module;
|
||||
|
||||
// Normalize fields for filters
|
||||
return {
|
||||
...recipe,
|
||||
persona: recipe.persona || null,
|
||||
action: recipe.action || null,
|
||||
extensions: Array.isArray(recipe.extensions) ? recipe.extensions : [],
|
||||
};
|
||||
});
|
||||
|
||||
if (query) {
|
||||
return allRecipes.filter((r) =>
|
||||
r.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
r.description.toLowerCase().includes(query.toLowerCase()) ||
|
||||
r.action?.toLowerCase().includes(query.toLowerCase()) ||
|
||||
r.activities?.some((activity) =>
|
||||
activity.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return allRecipes;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
darkMode: "class",
|
||||
darkMode: ["class", '[data-theme="dark"]'],
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user