diff --git a/frontend/components/layout/PageLayout.tsx b/frontend/components/layout/PageLayout.tsx new file mode 100644 index 0000000..ecc5b53 --- /dev/null +++ b/frontend/components/layout/PageLayout.tsx @@ -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 ( + + + + +
+ + {t("ui.app.title", "AutoMetabuilder Dashboard")} + + + {t("ui.dashboard.subtitle", "Control the bot and monitor system activity")} + +
+
+ {children} +
+
+ ); +} diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..a588d41 --- /dev/null +++ b/frontend/components/layout/Sidebar.tsx @@ -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 ( + + + + + {t("ui.app.name", "AutoMetabuilder")} + + + + + {items.map((item) => ( + onSelect(item.section)} + data-section={item.section} + sx={{ + color: selected === item.section ? "#fff" : "rgba(226,232,240,0.8)", + }} + > + + + ))} + + + + ); +} diff --git a/frontend/components/sections/DashboardSection.tsx b/frontend/components/sections/DashboardSection.tsx new file mode 100644 index 0000000..5208ae8 --- /dev/null +++ b/frontend/components/sections/DashboardSection.tsx @@ -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; + 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 ( + + + {t("ui.dashboard.title", "Dashboard")} + + + {t("ui.dashboard.subtitle", "Control the bot and monitor system activity")} + + + + + {t("ui.dashboard.bot_control", "Bot Control")} + + + setMode(event.target.value)} name="run-mode"> + {["once", "iterations", "yolo"].map((value) => ( + } + label={t(`ui.dashboard.run.${value}.title`, value === "iterations" ? "Repeat" : value.charAt(0).toUpperCase() + value.slice(1))} + /> + ))} + + + {mode === "iterations" && ( + setIterations(Number(event.target.value) || 1)} + sx={{ mt: 1, width: 140 }} + /> + )} + setStopAtMvp(event.target.checked)} />} + label={t("ui.dashboard.stop_mvp.title", "Stop at MVP")} + sx={{ 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")} + + + {feedback} + + + + + {t("ui.dashboard.logs.title", "Recent Logs")} + + + {logs.slice(-1200) || t("ui.dashboard.status.idle", "Idle")} + + + + + ); +} diff --git a/frontend/components/sections/PromptSection.tsx b/frontend/components/sections/PromptSection.tsx new file mode 100644 index 0000000..1622267 --- /dev/null +++ b/frontend/components/sections/PromptSection.tsx @@ -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; + 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 ( + + + {t("ui.prompt.title", "Prompt Builder")} + + + {t("ui.prompt.subtitle", "Shape how the assistant thinks, speaks, and decides")} + + setDraft(event.target.value)} + fullWidth + InputProps={{ + sx: { + backgroundColor: "#030712", + borderRadius: 2, + color: "white", + fontFamily: "JetBrains Mono, monospace", + }, + }} + /> + + + + {message} + + + + ); +} diff --git a/frontend/components/sections/SettingsSection.tsx b/frontend/components/sections/SettingsSection.tsx new file mode 100644 index 0000000..5ce2a12 --- /dev/null +++ b/frontend/components/sections/SettingsSection.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from "react"; +import { Button, Paper, Stack, TextField, Typography } from "@mui/material"; + +type SettingsSectionProps = { + envVars: Record; + onSave: (values: Record) => Promise; + t: (key: string, fallback?: string) => string; +}; + +export default function SettingsSection({ envVars, onSave, t }: SettingsSectionProps) { + const [values, setValues] = useState>(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 ( + + + {t("ui.settings.title", "Settings")} + + + {t("ui.settings.subtitle", "Configure services, security, and environment preferences")} + + + {Object.entries(values).map(([key, value]) => ( + updateField(key, event.target.value)} + InputProps={{ + sx: { + backgroundColor: "#030712", + borderRadius: 1, + color: "white", + }, + }} + /> + ))} + + + setNewKey(event.target.value)} + fullWidth + /> + setNewValue(event.target.value)} + fullWidth + /> + + + + + + {message} + + + + ); +} diff --git a/frontend/components/sections/TranslationsSection.tsx b/frontend/components/sections/TranslationsSection.tsx new file mode 100644 index 0000000..a91c8d9 --- /dev/null +++ b/frontend/components/sections/TranslationsSection.tsx @@ -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; + onRefresh: () => void; + t: (key: string, fallback?: string) => string; +}; + +export default function TranslationsSection({ languages, onRefresh, t }: TranslationsSectionProps) { + const [selected, setSelected] = useState(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 ( + + + {t("ui.translations.title", "Translations")} + + + {t("ui.translations.subtitle", "Create, edit, and maintain language files for bot messages")} + + + + {Object.entries(languages).map(([lang, label]) => ( + setSelected(lang)} + clickable + /> + ))} + + setEditorValue(event.target.value)} + InputProps={{ + sx: { + backgroundColor: "#030712", + borderRadius: 2, + color: "white", + fontFamily: "JetBrains Mono, monospace", + }, + }} + /> + + + + + + setNewLang(event.target.value)} + fullWidth + /> + + + + {(message || error) && ( + + {message || error} + + )} + + ); +} diff --git a/frontend/components/sections/WorkflowSection.tsx b/frontend/components/sections/WorkflowSection.tsx new file mode 100644 index 0000000..7e99fc7 --- /dev/null +++ b/frontend/components/sections/WorkflowSection.tsx @@ -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; + 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 ( + + + {t("ui.workflow.title", "Workflow Builder")} + + + {t("ui.workflow.subtitle", "Design the bot's task execution pipeline")} + + + + 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, + }} + /> + + + + {message} + + + + + + {t("ui.workflow.templates.title", "Workflow Templates")} + + + {packages.map((pkg) => ( + + + {pkg.label} + + {pkg.description} + + + + + ))} + + + + + ); +}