Files
clawpal/src/pages/Recipes.tsx
zhixian 3286ae3af1 feat: Discord guild/channel pickers, apply flow, agent identity
- Replace raw guild_id/channel_id text inputs with dropdown pickers
  showing human-readable names from openclaw config
- Add persistent file-level cache for Discord channel data with
  dedicated Channels tab and refresh button
- Read agent name/emoji from IDENTITY.md in workspace directories
- Rename Install→Cook throughout UI
- Add step-by-step apply flow: apply config → restart gateway → done
- Add global loading overlay for blocking operations
- Use react-diff-viewer-continued for config diff preview
- Fix validation bugs (Option<usize> null handling, discord type bypass)
- Fix serde camelCase on PreviewResult/ApplyResult structs
- Make slow commands async (refresh_discord, restart_gateway)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:37:28 +09:00

73 lines
2.3 KiB
TypeScript

import { useEffect, useReducer, useState } from "react";
import type { FormEvent } from "react";
import { api } from "../lib/api";
import { RecipeCard } from "../components/RecipeCard";
import { initialState, reducer } from "../lib/state";
import type { Recipe } from "../lib/types";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
export function Recipes({
onCook,
}: {
onCook: (id: string, source?: string) => void;
}) {
const [state, dispatch] = useReducer(reducer, initialState);
const [source, setSource] = useState("");
const [loadedSource, setLoadedSource] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const load = (nextSource: string) => {
setIsLoading(true);
const value = nextSource.trim();
api
.listRecipes(value || undefined)
.then((recipes) => {
setLoadedSource(value || undefined);
dispatch({ type: "setRecipes", recipes });
})
.catch(() => dispatch({ type: "setMessage", message: "Failed to load recipes" }))
.finally(() => setIsLoading(false));
};
useEffect(() => {
load("");
}, []);
const onLoadSource = (event: FormEvent) => {
event.preventDefault();
load(source);
};
return (
<section>
<h2 className="text-2xl font-bold mb-4">Recipes</h2>
<form onSubmit={onLoadSource} className="mb-2 flex items-center gap-2">
<Label>Recipe source (file path or URL)</Label>
<Input
value={source}
onChange={(event) => setSource(event.target.value)}
placeholder="/path/recipes.json or https://example.com/recipes.json"
className="w-[380px]"
/>
<Button type="submit" className="ml-2">
{isLoading ? "Loading..." : "Load"}
</Button>
</form>
<p className="text-sm text-muted-foreground mt-0">
Loaded from: {loadedSource || "builtin / clawpal recipes"}
</p>
<div className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
{state.recipes.map((recipe: Recipe) => (
<RecipeCard
key={recipe.id}
recipe={recipe}
onCook={() => onCook(recipe.id, loadedSource)}
/>
))}
</div>
</section>
);
}