Add workspaces, conversation sessions, app-managed computers and pictures
- Rail: Add icon with stacked form modals (bot, workspace, computer), coordinator, recent bots with "See all bots", Workspaces section, and a workspace mode listing only its crew - Workspaces: page for context, bots (per-workspace instructions and memory), computers (default, link, unlink), models and delete; conversations scoped per bot per workspace - Sessions: conversations split into searchable, reopenable sessions with carry-over summaries; sessions and new-session header icons - Computers: created and provisioned over the docker socket, home dir kept at DATA_DIR/computers/<id> after delete; super admins only - Models: workspace settings win over a bot's own, then the defaults - Pictures for bots and workspaces; bots editable from the header - Coordinator tools: update_workspace, set_bot_workspace_instructions Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import imageInputToBase64 from "@moduletrace/twui/utils/form/imageInputToBase64";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The avatar as it looks now (or will, with the picked picture).
|
||||
*/
|
||||
preview: ReactNode;
|
||||
has_image: boolean;
|
||||
onPick: (params: { data_url: string }) => void;
|
||||
onRemove: () => void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* An avatar preview with Upload/Change and Remove. The picked picture comes
|
||||
* back as a data URL; saving is up to the caller.
|
||||
*/
|
||||
export default function ImagePickField({
|
||||
preview,
|
||||
has_image,
|
||||
onPick,
|
||||
onRemove,
|
||||
loading,
|
||||
disabled,
|
||||
note = "PNG, JPG or WebP. A square picture looks best.",
|
||||
}: Props) {
|
||||
const input_ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<Row className="w-full gap-4! flex-nowrap items-center">
|
||||
{preview}
|
||||
<Stack className="gap-2! min-w-0">
|
||||
<Row className="gap-2!">
|
||||
<Button
|
||||
title={has_image ? "Change picture" : "Upload picture"}
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
size="small"
|
||||
loading={loading}
|
||||
disabled={disabled || loading}
|
||||
onClick={() => input_ref.current?.click()}
|
||||
>
|
||||
{has_image ? "Change picture" : "Upload picture"}
|
||||
</Button>
|
||||
{has_image ? (
|
||||
<Button
|
||||
title="Remove the picture"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
disabled={disabled || loading}
|
||||
onClick={() => {
|
||||
onRemove();
|
||||
if (input_ref.current) input_ref.current.value = "";
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
) : null}
|
||||
</Row>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">{note}</Span>
|
||||
</Stack>
|
||||
{/*
|
||||
* Raw on purpose: twui `Input` writes `value` on change, which
|
||||
* browsers forbid on file inputs, and `ImageUpload` is a full
|
||||
* card widget. This is only the hidden picker behind the button.
|
||||
*/}
|
||||
<input
|
||||
ref={input_ref}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
className="hidden"
|
||||
tabIndex={-1}
|
||||
aria-hidden
|
||||
onChange={(e) => {
|
||||
if (!e.target.files?.[0]) return;
|
||||
|
||||
imageInputToBase64({ imageInput: e.target }).then((res) => {
|
||||
if (res?.imageBase64Full) onPick({ data_url: res.imageBase64Full });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user