diff --git a/src/components/general/back-button.tsx b/src/components/general/back-button.tsx new file mode 100644 index 0000000..8626d62 --- /dev/null +++ b/src/components/general/back-button.tsx @@ -0,0 +1,21 @@ +import Button from "@moduletrace/twui/layout/Button"; +import { ChevronLeft, type LucideIcon } from "lucide-react"; + +/** + * Back Button + */ +export default function BackButton() { + return ( + + ); +} diff --git a/src/components/general/logo.tsx b/src/components/general/logo.tsx index 20baeea..26ac8cb 100644 --- a/src/components/general/logo.tsx +++ b/src/components/general/logo.tsx @@ -13,6 +13,7 @@ type Props = { * Only `className` applies: it sets the wordmark colour. */ text_props?: ComponentProps; + no_link?: boolean; }; /** @@ -22,21 +23,30 @@ export default function Logo({ icon_size = 36, text_props, text_wrapper_props, + no_link, }: Props) { + const content = ( + + + + + + + ); + + if (no_link) { + return content; + } + return ( - - - - - - + {content} ); } diff --git a/src/data/app-data.ts b/src/data/app-data.ts index f494144..e2a88b5 100644 --- a/src/data/app-data.ts +++ b/src/data/app-data.ts @@ -240,7 +240,7 @@ export const AppData = { /** * Model turns in one text run before it must stop and report. */ - HarnessMaxToolSteps: 30, + HarnessMaxToolSteps: 50, /** * Longest tool result handed back to the model. */ @@ -462,7 +462,8 @@ export const AppData = { /** * What a speech model reads when a voice is tried in the model settings. */ - VoicePreviewText: "Hi there. This is how I'll sound when I read my replies aloud.", + VoicePreviewText: + "Hi there. This is how I'll sound when I read my replies aloud.", /** * Largest picture a bot or a workspace can have. */ diff --git a/src/dict/settings-record-sections-dict.ts b/src/dict/settings-record-sections-dict.ts index 14eff17..8476719 100644 --- a/src/dict/settings-record-sections-dict.ts +++ b/src/dict/settings-record-sections-dict.ts @@ -41,7 +41,7 @@ export const SettingsRecordSections: Record< { title: "Models", url: "#models", Icon: CpuAi }, { title: "Workspaces", url: "#workspaces", Icon: Layers }, { title: "Computers", url: "#computers", Icon: Monitor }, - { title: "Delete", url: "#delete", Icon: Trash2 }, + { title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert }, ], workspaces: [ { title: "Picture", url: "#picture", Icon: Image }, @@ -49,7 +49,7 @@ export const SettingsRecordSections: Record< { title: "Bots", url: "#bots", Icon: Bot }, { title: "Computers", url: "#computers", Icon: Monitor }, { title: "Models", url: "#models", Icon: CpuAi }, - { title: "Delete", url: "#delete", Icon: Trash2 }, + { title: "Danger Zone", url: "#danger-zone", Icon: CircleAlert }, ], users: [ { title: "Overview", url: "#overview", Icon: Info }, diff --git a/src/functions/backend/storage/resolve-storage-path.ts b/src/functions/backend/storage/resolve-storage-path.ts index 080af20..b149ce8 100644 --- a/src/functions/backend/storage/resolve-storage-path.ts +++ b/src/functions/backend/storage/resolve-storage-path.ts @@ -3,6 +3,8 @@ import path from "node:path"; import { AppData } from "@/src/data/app-data"; import AppError from "@/src/utils/app-error"; +const empty_folder_text = `Empty Folder`; + type Params = { /** * The volume's absolute root (`resolveStorageSpec`'s `root`), already @@ -48,7 +50,10 @@ export type ResolvedStoragePath = { * the bot's tools); a caller that needs an existing file checks it * afterwards. */ -export default function resolveStoragePath({ root, path: requested }: Params): ResolvedStoragePath { +export default function resolveStoragePath({ + root, + path: requested, +}: Params): ResolvedStoragePath { const resolved_root = path.resolve(root); const raw = (requested || "").trim().replace(/\\/g, "/"); @@ -57,19 +62,26 @@ export default function resolveStoragePath({ root, path: requested }: Params): R } if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) { - throw new AppError(`Use a path inside the folder, not an absolute one.`); + throw new AppError( + `Use a path inside the folder, not an absolute one.`, + ); } - const segments = raw.split("/").map((segment) => segment.trim()).filter(Boolean); + const segments = raw + .split("/") + .map((segment) => segment.trim()) + .filter(Boolean); for (const segment of segments) { if (segment == "." || segment == "..") { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } } if (segments.length > AppData.BotFilePathMaxDepth) { - throw new AppError(`That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`); + throw new AppError( + `That path is too deep: at most ${AppData.BotFilePathMaxDepth} folders.`, + ); } const relative = segments.join("/"); @@ -81,7 +93,7 @@ export default function resolveStoragePath({ root, path: requested }: Params): R const absolute = path.join(resolved_root, ...segments); if (!isInsideRoot({ root: resolved_root, absolute })) { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } guardRealPath({ root: resolved_root, absolute }); @@ -99,7 +111,9 @@ export default function resolveStoragePath({ root, path: requested }: Params): R * symlink (or any other reason). */ function guardRealPath({ root, absolute }: { root: string; absolute: string }) { - const real_root = existsSync(root) ? realpathSync(root) : path.resolve(root); + const real_root = existsSync(root) + ? realpathSync(root) + : path.resolve(root); let probe = absolute; while (!existsSync(probe)) { @@ -113,16 +127,25 @@ function guardRealPath({ root, absolute }: { root: string; absolute: string }) { const real_probe = realpathSync(probe); if (!isInsideRoot({ root: real_root, absolute: real_probe })) { - throw new AppError(`That path isn't valid: it must stay inside the folder.`); + throw new AppError(empty_folder_text); } } /** * Whether a path is inside a root, before it exists. */ -export function isInsideRoot({ root, absolute }: { root: string; absolute: string }) { +export function isInsideRoot({ + root, + absolute, +}: { + root: string; + absolute: string; +}) { const resolved_root = path.resolve(root); const resolved = path.resolve(absolute); - return resolved == resolved_root || resolved.startsWith(`${resolved_root}${path.sep}`); + return ( + resolved == resolved_root || + resolved.startsWith(`${resolved_root}${path.sep}`) + ); } diff --git a/src/layouts/bots/(partials)/rail-back-link.tsx b/src/layouts/bots/(partials)/rail-back-link.tsx index f066bec..b4523e3 100644 --- a/src/layouts/bots/(partials)/rail-back-link.tsx +++ b/src/layouts/bots/(partials)/rail-back-link.tsx @@ -17,6 +17,8 @@ export default function RailBackLink({ title = `Back to ${SiteData["SiteSlug"]}`, href = AdminHomePath, }: Props) { + return null; + return ( + - - + {pageProps?.url?.pathname.startsWith(`/admin/bots/`) ? ( + <> + + + + ) : ( + <> + + + )} {show_add ? : null} diff --git a/src/layouts/bots/(partials)/rail-pages-list.tsx b/src/layouts/bots/(partials)/rail-pages-list.tsx index 823d14f..37eeb03 100644 --- a/src/layouts/bots/(partials)/rail-pages-list.tsx +++ b/src/layouts/bots/(partials)/rail-pages-list.tsx @@ -17,17 +17,24 @@ type Props = { export default function RailPagesList({ pathname }: Props) { const { pageProps } = useContext(AppContext); const current = pathname?.replace(/\/$/, ""); - const is_super_admin = isPassThroughUser({ user_types: pageProps?.user_types }); + const is_super_admin = isPassThroughUser({ + user_types: pageProps?.user_types, + }); return ( <> - - {RailPages.filter((page) => is_super_admin || !page.super_admin_only).map((page) => ( + {/* */} + {RailPages.filter( + (page) => is_super_admin || !page.super_admin_only, + ).map((page) => ( ))} diff --git a/src/layouts/bots/(partials)/rail-settings-aside.tsx b/src/layouts/bots/(partials)/rail-settings-aside.tsx index 70eec7e..180fa93 100644 --- a/src/layouts/bots/(partials)/rail-settings-aside.tsx +++ b/src/layouts/bots/(partials)/rail-settings-aside.tsx @@ -64,7 +64,8 @@ export default function RailSettingsAside({ pathname }: Props) { ? Boolean(pageProps?.bot_harness_overview?.harnesses?.length) : record.kind == "workspaces" ? Boolean( - pageProps?.workspace_harness_overview?.harnesses?.length, + pageProps?.workspace_harness_overview?.harnesses + ?.length, ) : true; } @@ -101,9 +102,7 @@ export default function RailSettingsAside({ pathname }: Props) { title = workspace.name || `Workspace ${workspace.id}`; subtitle = workspace.description || "A workspace"; - avatar = ( - - ); + avatar = ; } else if (record.kind == "users") { const person = pageProps?.target_person; @@ -139,7 +138,7 @@ export default function RailSettingsAside({ pathname }: Props) { return ( <> - + {/* */} - + {/* */} {SettingsPages.filter((page) => page.can_view ? page.can_view({ user_types: pageProps?.user_types }) diff --git a/src/layouts/bots/(partials)/rail-workspace-list.tsx b/src/layouts/bots/(partials)/rail-workspace-list.tsx index 87b2bb8..ec7a8cd 100644 --- a/src/layouts/bots/(partials)/rail-workspace-list.tsx +++ b/src/layouts/bots/(partials)/rail-workspace-list.tsx @@ -29,7 +29,12 @@ type Props = { * page), the coordinator working in it, then the workspace's bots. Every * conversation here runs with the workspace's context. */ -export default function RailWorkspaceList({ workspace, bots, selected_id, on_workspace_page }: Props) { +export default function RailWorkspaceList({ + workspace, + bots, + selected_id, + on_workspace_page, +}: Props) { const { openLinkBots } = useContext(AddFlowContext); const coordinator = bots.find((bot) => bot.kind === "coordinator"); const workers = bots.filter((bot) => bot.kind !== "coordinator"); @@ -38,11 +43,16 @@ export default function RailWorkspaceList({ workspace, bots, selected_id, on_wor return ( <> - - + {/* */} + {coordinator ? ( ) : null} } + action={ + + } title="Workspace bots" workers={workers} selected_id={on_workspace_page ? undefined : selected_id} diff --git a/src/layouts/main/(sections)/Header.tsx b/src/layouts/main/(sections)/Header.tsx index 0fe036c..18aee97 100755 --- a/src/layouts/main/(sections)/Header.tsx +++ b/src/layouts/main/(sections)/Header.tsx @@ -53,4 +53,4 @@ export default function Header() { ); -} \ No newline at end of file +} diff --git a/src/pages/__root.tsx b/src/pages/__root.tsx index 7b45f51..ac00493 100755 --- a/src/pages/__root.tsx +++ b/src/pages/__root.tsx @@ -79,9 +79,6 @@ export default function App({ return ( + {is_workspace ? : null}

diff --git a/src/pages/admin/settings/(partials)/settings-pane-header.tsx b/src/pages/admin/settings/(partials)/settings-pane-header.tsx index 99393fc..ba1b18b 100644 --- a/src/pages/admin/settings/(partials)/settings-pane-header.tsx +++ b/src/pages/admin/settings/(partials)/settings-pane-header.tsx @@ -4,6 +4,8 @@ import Stack from "@moduletrace/twui/layout/Stack"; import H1 from "@moduletrace/twui/layout/H1"; import Span from "@moduletrace/twui/layout/Span"; import RailDrawerButton from "@/src/layouts/bots/(partials)/rail-drawer-button"; +import BackButton from "@/src/components/general/back-button"; +import { twMerge } from "tailwind-merge"; type Props = { title: string; @@ -25,13 +27,29 @@ export default function SettingsPaneHeader({ action, }: Props) { return ( - + + -

+

{title}

- + {description}
diff --git a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx index 192cc49..bc723b2 100644 --- a/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx +++ b/src/pages/admin/settings/bots/[bot_id]/(partials)/bot-delete-group.tsx @@ -45,9 +45,9 @@ export default function BotDeleteGroup({ bot }: Props) { return ( - + ); }