mirror of
https://github.com/johndoe6345789/AutoMetabuilder.git
synced 2026-04-24 13:54:59 +00:00
Introduce AutoMetabuilder core components and workflow packages:
- Implement core components: CLI argument parsing, environment loading, GitHub service creation, and logging configuration. - Add support for OpenAI client setup and model resolution. - Develop SDLC context loader from GitHub and repository files. - Implement workflow context and engine builders. - Introduce major workflow packages: `game_tick_loop` and `contextual_iterative_loop`. - Update localization files with new package descriptions and labels. - Streamline web navigation by loading items from a dedicated JSON file.
This commit is contained in:
33
frontend/components/layout/PageLayout.tsx
Normal file
33
frontend/components/layout/PageLayout.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Box, Toolbar, Typography } from "@mui/material";
|
||||
import { NavigationItem } from "../../lib/types";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
type PageLayoutProps = {
|
||||
navItems: NavigationItem[];
|
||||
section: string;
|
||||
onSectionChange: (section: string) => void;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function PageLayout({ navItems, section, onSectionChange, t, children }: PageLayoutProps) {
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Sidebar items={navItems} selected={section} onSelect={onSectionChange} t={t} />
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3, bgcolor: "#04070f", minHeight: "100vh" }}>
|
||||
<Toolbar disableGutters>
|
||||
<div>
|
||||
<Typography variant="h4" color="text.primary" gutterBottom>
|
||||
{t("ui.app.title", "AutoMetabuilder Dashboard")}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
{t("ui.dashboard.subtitle", "Control the bot and monitor system activity")}
|
||||
</Typography>
|
||||
</div>
|
||||
</Toolbar>
|
||||
<Box>{children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
39
frontend/components/layout/Sidebar.tsx
Normal file
39
frontend/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Box, Divider, Drawer, List, ListItemButton, ListItemText, Typography } from "@mui/material";
|
||||
import { NavigationItem } from "../../lib/types";
|
||||
|
||||
type SidebarProps = {
|
||||
items: NavigationItem[];
|
||||
selected: string;
|
||||
onSelect: (section: string) => void;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function Sidebar({ items, selected, onSelect, t }: SidebarProps) {
|
||||
return (
|
||||
<Drawer variant="permanent" anchor="left" sx={{ width: 220, flexShrink: 0 }}>
|
||||
<Box sx={{ height: "100%", backgroundColor: "#0f172a" }}>
|
||||
<Box sx={{ px: 3, py: 2 }}>
|
||||
<Typography variant="overline" color="text.secondary">
|
||||
{t("ui.app.name", "AutoMetabuilder")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ borderColor: "rgba(255,255,255,0.08)" }} />
|
||||
<List>
|
||||
{items.map((item) => (
|
||||
<ListItemButton
|
||||
key={item.section}
|
||||
selected={selected === item.section}
|
||||
onClick={() => onSelect(item.section)}
|
||||
data-section={item.section}
|
||||
sx={{
|
||||
color: selected === item.section ? "#fff" : "rgba(226,232,240,0.8)",
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={t(item.label_key, item.default_label)} primaryTypographyProps={{ fontSize: 14 }} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
113
frontend/components/sections/DashboardSection.tsx
Normal file
113
frontend/components/sections/DashboardSection.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Paper,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { UIStatus } from "../../lib/types";
|
||||
import { emitWebhook } from "../../hooks/useWebhook";
|
||||
|
||||
type DashboardSectionProps = {
|
||||
status: UIStatus;
|
||||
logs: string;
|
||||
onRun: (payload: { mode?: string; iterations?: number; yolo?: boolean; stop_at_mvp?: boolean }) => Promise<void>;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function DashboardSection({ status, logs, onRun, t }: DashboardSectionProps) {
|
||||
const [mode, setMode] = useState("once");
|
||||
const [iterations, setIterations] = useState(1);
|
||||
const [stopAtMvp, setStopAtMvp] = useState(false);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const handleRun = async () => {
|
||||
const isYolo = mode === "yolo";
|
||||
setFeedback(t("ui.dashboard.status.bot_label", "Bot Status") + " — submitting");
|
||||
try {
|
||||
await onRun({ mode, iterations, yolo: isYolo, stop_at_mvp: stopAtMvp });
|
||||
setFeedback(t("ui.dashboard.start_bot", "Start Bot") + " " + t("ui.dashboard.status.running", "Running"));
|
||||
emitWebhook("botRunComplete", { mode, iterations });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setFeedback(t("ui.dashboard.status.idle", "Idle"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper id="dashboard" sx={{ p: 3, mb: 3, backgroundColor: "#0d111b" }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{t("ui.dashboard.title", "Dashboard")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{t("ui.dashboard.subtitle", "Control the bot and monitor system activity")}
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={3} mt={2}>
|
||||
<Paper sx={{ flex: 1, p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.dashboard.bot_control", "Bot Control")}
|
||||
</Typography>
|
||||
<FormControl component="fieldset">
|
||||
<RadioGroup row value={mode} onChange={(event) => setMode(event.target.value)} name="run-mode">
|
||||
{["once", "iterations", "yolo"].map((value) => (
|
||||
<FormControlLabel
|
||||
key={value}
|
||||
value={value}
|
||||
control={<Radio size="small" />}
|
||||
label={t(`ui.dashboard.run.${value}.title`, value === "iterations" ? "Repeat" : value.charAt(0).toUpperCase() + value.slice(1))}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{mode === "iterations" && (
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
label={t("ui.dashboard.run.repeat.label", "Iterations")}
|
||||
value={iterations}
|
||||
onChange={(event) => setIterations(Number(event.target.value) || 1)}
|
||||
sx={{ mt: 1, width: 140 }}
|
||||
/>
|
||||
)}
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={stopAtMvp} onChange={(event) => setStopAtMvp(event.target.checked)} />}
|
||||
label={t("ui.dashboard.stop_mvp.title", "Stop at MVP")}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleRun}
|
||||
disabled={status.is_running}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{t("ui.dashboard.start_bot", "Start Bot")}
|
||||
</Button>
|
||||
<Typography variant="caption" color="text.secondary" display="block" mt={1}>
|
||||
{status.is_running ? t("ui.dashboard.status.running", "Running") : t("ui.dashboard.status.idle", "Idle")} •
|
||||
{" "}
|
||||
{status.mvp_reached ? t("ui.dashboard.status.mvp_reached", "Reached") : t("ui.dashboard.status.mvp_progress", "In Progress")}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="primary">
|
||||
{feedback}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper sx={{ flex: 1, p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.dashboard.logs.title", "Recent Logs")}
|
||||
</Typography>
|
||||
<Box component="pre" sx={{ maxHeight: 240, overflow: "auto", fontSize: 12, color: "white" }}>
|
||||
{logs.slice(-1200) || t("ui.dashboard.status.idle", "Idle")}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
57
frontend/components/sections/PromptSection.tsx
Normal file
57
frontend/components/sections/PromptSection.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Paper, Stack, TextField, Typography } from "@mui/material";
|
||||
|
||||
type PromptSectionProps = {
|
||||
content: string;
|
||||
onSave: (content: string) => Promise<void>;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function PromptSection({ content, onSave, t }: PromptSectionProps) {
|
||||
const [draft, setDraft] = useState(content);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(content);
|
||||
}, [content]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(draft);
|
||||
setMessage(t("ui.prompt.save", "Save Prompt"));
|
||||
setTimeout(() => setMessage(""), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper id="prompt" sx={{ p: 3, mb: 3, backgroundColor: "#0d111b" }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{t("ui.prompt.title", "Prompt Builder")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{t("ui.prompt.subtitle", "Shape how the assistant thinks, speaks, and decides")}
|
||||
</Typography>
|
||||
<TextField
|
||||
multiline
|
||||
minRows={10}
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
backgroundColor: "#030712",
|
||||
borderRadius: 2,
|
||||
color: "white",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={2} alignItems="center" mt={2}>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
{t("ui.prompt.save", "Save Prompt")}
|
||||
</Button>
|
||||
<Typography variant="body2" color="success.main">
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
89
frontend/components/sections/SettingsSection.tsx
Normal file
89
frontend/components/sections/SettingsSection.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Paper, Stack, TextField, Typography } from "@mui/material";
|
||||
|
||||
type SettingsSectionProps = {
|
||||
envVars: Record<string, string>;
|
||||
onSave: (values: Record<string, string>) => Promise<void>;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function SettingsSection({ envVars, onSave, t }: SettingsSectionProps) {
|
||||
const [values, setValues] = useState<Record<string, string>>(envVars);
|
||||
const [newKey, setNewKey] = useState("");
|
||||
const [newValue, setNewValue] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setValues(envVars);
|
||||
}, [envVars]);
|
||||
|
||||
const updateField = (key: string, next: string) => {
|
||||
setValues((prev) => ({ ...prev, [key]: next }));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(values);
|
||||
setMessage(t("ui.settings.save_all", "Save All Settings"));
|
||||
setTimeout(() => setMessage(""), 2000);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!newKey.trim()) return;
|
||||
updateField(newKey.trim(), newValue);
|
||||
setNewKey("");
|
||||
setNewValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper id="settings" sx={{ p: 3, mb: 3, backgroundColor: "#0d111b" }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{t("ui.settings.title", "Settings")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{t("ui.settings.subtitle", "Configure services, security, and environment preferences")}
|
||||
</Typography>
|
||||
<Stack spacing={2} mt={2}>
|
||||
{Object.entries(values).map(([key, value]) => (
|
||||
<TextField
|
||||
key={key}
|
||||
label={key}
|
||||
value={value}
|
||||
onChange={(event) => updateField(key, event.target.value)}
|
||||
InputProps={{
|
||||
sx: {
|
||||
backgroundColor: "#030712",
|
||||
borderRadius: 1,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
label={t("ui.settings.add.placeholder_key", "KEY")}
|
||||
value={newKey}
|
||||
onChange={(event) => setNewKey(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("ui.settings.add.placeholder_value", "Value")}
|
||||
value={newValue}
|
||||
onChange={(event) => setNewValue(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<Button variant="outlined" onClick={handleAdd}>
|
||||
{t("ui.actions.add", "Add")}
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} alignItems="center" mt={3}>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
{t("ui.settings.save_all", "Save All Settings")}
|
||||
</Button>
|
||||
<Typography variant="body2" color="success.main">
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
138
frontend/components/sections/TranslationsSection.tsx
Normal file
138
frontend/components/sections/TranslationsSection.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
createTranslation,
|
||||
deleteTranslation,
|
||||
fetchTranslation,
|
||||
updateTranslation,
|
||||
} from "../../lib/api";
|
||||
|
||||
type TranslationsSectionProps = {
|
||||
languages: Record<string, string>;
|
||||
onRefresh: () => void;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function TranslationsSection({ languages, onRefresh, t }: TranslationsSectionProps) {
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [editorValue, setEditorValue] = useState("{}");
|
||||
const [message, setMessage] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [newLang, setNewLang] = useState("");
|
||||
|
||||
const loadContent = async (lang: string) => {
|
||||
setError("");
|
||||
const data = await fetchTranslation(lang);
|
||||
setEditorValue(JSON.stringify(data.content, null, 2));
|
||||
};
|
||||
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
if (!selected && Object.keys(languages).length) {
|
||||
setSelected(Object.keys(languages)[0]);
|
||||
}
|
||||
}, [languages, selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
loadContent(selected);
|
||||
}
|
||||
}, [selected]);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selected) return;
|
||||
try {
|
||||
const payload = JSON.parse(editorValue);
|
||||
await updateTranslation(selected, payload);
|
||||
setMessage(t("ui.translations.notice.saved", "Translation saved!"));
|
||||
onRefresh();
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!newLang.trim()) return;
|
||||
await createTranslation(newLang.trim());
|
||||
setNewLang("");
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selected) return;
|
||||
await deleteTranslation(selected);
|
||||
setSelected(null);
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper id="translations" sx={{ p: 3, mb: 3, backgroundColor: "#0d111b" }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{t("ui.translations.title", "Translations")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{t("ui.translations.subtitle", "Create, edit, and maintain language files for bot messages")}
|
||||
</Typography>
|
||||
<Stack spacing={2} sx={{ mt: 2 }}>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||
{Object.entries(languages).map(([lang, label]) => (
|
||||
<Chip
|
||||
label={`${lang} (${label})`}
|
||||
key={lang}
|
||||
variant={selected === lang ? "filled" : "outlined"}
|
||||
onClick={() => setSelected(lang)}
|
||||
clickable
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<TextField
|
||||
multiline
|
||||
minRows={12}
|
||||
value={editorValue}
|
||||
onChange={(event) => setEditorValue(event.target.value)}
|
||||
InputProps={{
|
||||
sx: {
|
||||
backgroundColor: "#030712",
|
||||
borderRadius: 2,
|
||||
color: "white",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} mt={2}>
|
||||
<Button variant="contained" onClick={handleSave} disabled={!selected}>
|
||||
{t("ui.actions.save", "Save")}
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleDelete} disabled={!selected}>
|
||||
{t("ui.actions.delete", "Delete")}
|
||||
</Button>
|
||||
<Stack component="form" onSubmit={handleCreate} direction="row" spacing={1} flex={1}>
|
||||
<TextField
|
||||
placeholder={t("ui.translations.add_language_placeholder", "Add language...")}
|
||||
value={newLang}
|
||||
onChange={(event) => setNewLang(event.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<Button type="submit" variant="contained">
|
||||
{t("ui.actions.add", "Add")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{(message || error) && (
|
||||
<Typography variant="body2" color={error ? "error.main" : "success.main"} mt={2}>
|
||||
{message || error}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
84
frontend/components/sections/WorkflowSection.tsx
Normal file
84
frontend/components/sections/WorkflowSection.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Button, Paper, Stack, Typography } from "@mui/material";
|
||||
import { WorkflowPackageSummary } from "../../lib/types";
|
||||
|
||||
type WorkflowSectionProps = {
|
||||
content: string;
|
||||
packages: WorkflowPackageSummary[];
|
||||
onSave: (content: string) => Promise<void>;
|
||||
onTemplateSelect: (id: string) => void;
|
||||
t: (key: string, fallback?: string) => string;
|
||||
};
|
||||
|
||||
export default function WorkflowSection({ content, packages, onSave, onTemplateSelect, t }: WorkflowSectionProps) {
|
||||
const [draft, setDraft] = useState(content);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(content);
|
||||
}, [content]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(draft);
|
||||
setMessage(t("ui.workflow.save", "Save Workflow") + " " + t("ui.actions.save", "Save"));
|
||||
setTimeout(() => setMessage(""), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper id="workflow" sx={{ p: 3, mb: 3, backgroundColor: "#0d111b" }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{t("ui.workflow.title", "Workflow Builder")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{t("ui.workflow.subtitle", "Design the bot's task execution pipeline")}
|
||||
</Typography>
|
||||
<Stack direction={{ xs: "column", md: "row" }} spacing={3}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
component="textarea"
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
rows={18}
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
backgroundColor: "#030712",
|
||||
border: "1px solid rgba(148, 163, 184, 0.4)",
|
||||
borderRadius: 2,
|
||||
color: "white",
|
||||
p: 2,
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={1} alignItems="center" mt={2}>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
{t("ui.workflow.save", "Save Workflow")}
|
||||
</Button>
|
||||
<Typography variant="body2" color="success.main">
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Paper sx={{ flex: 1, p: 2, backgroundColor: "#0b1221" }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("ui.workflow.templates.title", "Workflow Templates")}
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{packages.map((pkg) => (
|
||||
<Paper key={pkg.id} variant="outlined" sx={{ p: 1 }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2">{pkg.label}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pkg.description}
|
||||
</Typography>
|
||||
<Button size="small" variant="text" onClick={() => onTemplateSelect(pkg.id)}>
|
||||
{t("ui.workflow.templates.apply", "Apply Template")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user