Normalize screen layouts: extract control groups, unify sidebar widths, and refactor PromptsScreen

- Extract ToolControls group from ToolsScreen (search, list, list-changed indicator)
- Extract PromptControls and PromptListItem groups for PromptsScreen sidebar
- Refactor PromptsScreen to sidebar + detail layout (matching ResourcesScreen pattern)
- Refactor PromptArgumentsForm to accept a single prompt instead of a dropdown
- Normalize all screen sidebar widths to 340px (Tools, Resources, Prompts, Logs, Tasks, History)
- Convert ToolsScreen from Grid-only to sidebar + Grid layout
- Replace LogControls checkboxes with full-width toggle buttons
- Move Clear/Export buttons from LogControls to LogStreamPanel toolbar
- Make PromptArgumentsForm "Get Prompt" button right-aligned and compact
- Update all affected stories and ConnectedView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-31 17:54:20 -04:00
commit a6724626b7
19 changed files with 627 additions and 233 deletions

View file

@ -20,8 +20,7 @@ const meta: Meta<typeof LogControls> = {
onSetLevel: fn(), onSetLevel: fn(),
onFilterChange: fn(), onFilterChange: fn(),
onToggleLevel: fn(), onToggleLevel: fn(),
onClear: fn(), onToggleAllLevels: fn(),
onExport: fn(),
}, },
}; };

View file

@ -1,12 +1,12 @@
import { import {
Button, Button,
Checkbox,
Group, Group,
Select, Select,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title, Title,
UnstyledButton,
} from "@mantine/core"; } from "@mantine/core";
const LOG_LEVELS = [ const LOG_LEVELS = [
@ -20,22 +20,17 @@ const LOG_LEVELS = [
"emergency", "emergency",
] as const; ] as const;
const LEVEL_COLORS: Record<string, { c: string; fw?: number }> = { const LEVEL_COLORS: Record<string, { c: string }> = {
debug: { c: "dimmed" }, debug: { c: "dimmed" },
info: { c: "blue" }, info: { c: "blue" },
notice: { c: "teal" }, notice: { c: "teal" },
warning: { c: "yellow" }, warning: { c: "yellow" },
error: { c: "red" }, error: { c: "red" },
critical: { c: "red", fw: 700 }, critical: { c: "red" },
alert: { c: "red", fw: 700 }, alert: { c: "red" },
emergency: { c: "red", fw: 900 }, emergency: { c: "red" },
}; };
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
const SubtleButton = Button.withProps({ const SubtleButton = Button.withProps({
variant: "subtle", variant: "subtle",
size: "xs", size: "xs",
@ -49,8 +44,6 @@ export interface LogControlsProps {
onFilterChange: (text: string) => void; onFilterChange: (text: string) => void;
onToggleLevel: (level: string, visible: boolean) => void; onToggleLevel: (level: string, visible: boolean) => void;
onToggleAllLevels: () => void; onToggleAllLevels: () => void;
onClear: () => void;
onExport: () => void;
} }
export function LogControls({ export function LogControls({
@ -61,8 +54,6 @@ export function LogControls({
onFilterChange, onFilterChange,
onToggleLevel, onToggleLevel,
onToggleAllLevels, onToggleAllLevels,
onClear,
onExport,
}: LogControlsProps) { }: LogControlsProps) {
return ( return (
<Stack gap="md"> <Stack gap="md">
@ -71,6 +62,7 @@ export function LogControls({
<Title order={5}>Active Level</Title> <Title order={5}>Active Level</Title>
<Group wrap="nowrap"> <Group wrap="nowrap">
<Select <Select
flex={1}
data={LOG_LEVELS.map((level) => ({ value: level, label: level }))} data={LOG_LEVELS.map((level) => ({ value: level, label: level }))}
value={currentLevel} value={currentLevel}
onChange={(value) => { onChange={(value) => {
@ -97,26 +89,24 @@ export function LogControls({
: "Select All"} : "Select All"}
</SubtleButton> </SubtleButton>
</Group> </Group>
{LOG_LEVELS.map((level) => { <Stack gap="xs">
const style = LEVEL_COLORS[level]; {LOG_LEVELS.map((level) => {
return ( const style = LEVEL_COLORS[level];
<Checkbox const active = !!visibleLevels[level];
key={level} return (
checked={!!visibleLevels[level]} <UnstyledButton
onChange={(e) => onToggleLevel(level, e.currentTarget.checked)} key={level}
label={ w="100%"
<Text c={style.c} fw={style.fw}> p="sm"
{level} variant="listItem"
</Text> bg={active ? "var(--mantine-primary-color-light)" : undefined}
} onClick={() => onToggleLevel(level, !active)}
/> >
); <Text c={style.c} ta="center" fw={500}>{level}</Text>
})} </UnstyledButton>
);
<Group> })}
<ToolbarButton onClick={onClear}>Clear</ToolbarButton> </Stack>
<ToolbarButton onClick={onExport}>Export</ToolbarButton>
</Group>
</Stack> </Stack>
); );
} }

View file

@ -21,6 +21,8 @@ const meta: Meta<typeof LogStreamPanel> = {
autoScroll: true, autoScroll: true,
onToggleAutoScroll: fn(), onToggleAutoScroll: fn(),
onCopyAll: fn(), onCopyAll: fn(),
onClear: fn(),
onExport: fn(),
}, },
}; };

View file

@ -19,6 +19,8 @@ export interface LogStreamPanelProps {
autoScroll: boolean; autoScroll: boolean;
onToggleAutoScroll: () => void; onToggleAutoScroll: () => void;
onCopyAll: () => void; onCopyAll: () => void;
onClear: () => void;
onExport: () => void;
} }
const PanelContainer = Paper.withProps({ const PanelContainer = Paper.withProps({
@ -61,6 +63,8 @@ export function LogStreamPanel({
autoScroll, autoScroll,
onToggleAutoScroll, onToggleAutoScroll,
onCopyAll, onCopyAll,
onClear,
onExport,
}: LogStreamPanelProps) { }: LogStreamPanelProps) {
const filteredEntries = useMemo( const filteredEntries = useMemo(
() => entries.filter((e) => matchesFilters(e, filterText, visibleLevels)), () => entries.filter((e) => matchesFilters(e, filterText, visibleLevels)),
@ -77,6 +81,8 @@ export function LogStreamPanel({
checked={autoScroll} checked={autoScroll}
onChange={onToggleAutoScroll} onChange={onToggleAutoScroll}
/> />
<ToolbarButton onClick={onClear}>Clear</ToolbarButton>
<ToolbarButton onClick={onExport}>Export</ToolbarButton>
<ToolbarButton onClick={onCopyAll}>Copy All</ToolbarButton> <ToolbarButton onClick={onCopyAll}>Copy All</ToolbarButton>
</Group> </Group>
</Group> </Group>

View file

@ -6,7 +6,6 @@ const meta: Meta<typeof PromptArgumentsForm> = {
title: "Groups/PromptArgumentsForm", title: "Groups/PromptArgumentsForm",
component: PromptArgumentsForm, component: PromptArgumentsForm,
args: { args: {
onSelectPrompt: fn(),
onArgumentChange: fn(), onArgumentChange: fn(),
onGetPrompt: fn(), onGetPrompt: fn(),
}, },
@ -15,31 +14,19 @@ const meta: Meta<typeof PromptArgumentsForm> = {
export default meta; export default meta;
type Story = StoryObj<typeof PromptArgumentsForm>; type Story = StoryObj<typeof PromptArgumentsForm>;
const samplePrompts = [ export const NoArguments: Story = {
{ args: {
name: "summarize", name: "summarize",
description: "Summarize the given text into key points", description: "Summarize the given text into key points",
},
{
name: "translate",
description: "Translate text from one language to another",
},
{ name: "analyze", description: "Analyze sentiment and tone of the text" },
];
export const NoSelection: Story = {
args: {
prompts: samplePrompts,
selectedPrompt: undefined,
arguments: [], arguments: [],
argumentValues: {}, argumentValues: {},
}, },
}; };
export const Selected: Story = { export const WithArguments: Story = {
args: { args: {
prompts: samplePrompts, name: "translate",
selectedPrompt: "translate", description: "Translate text from one language to another",
arguments: [ arguments: [
{ name: "text", required: true, description: "The text to translate" }, { name: "text", required: true, description: "The text to translate" },
{ {
@ -52,10 +39,10 @@ export const Selected: Story = {
}, },
}; };
export const WithRequiredArgs: Story = { export const WithRequiredAndOptional: Story = {
args: { args: {
prompts: samplePrompts, name: "summarize",
selectedPrompt: "summarize", description: "Summarize the given text into key points",
arguments: [ arguments: [
{ name: "text", required: true, description: "The text to summarize" }, { name: "text", required: true, description: "The text to summarize" },
{ {
@ -75,8 +62,8 @@ export const WithRequiredArgs: Story = {
export const AllFilled: Story = { export const AllFilled: Story = {
args: { args: {
prompts: samplePrompts, name: "translate",
selectedPrompt: "translate", description: "Translate text from one language to another",
arguments: [ arguments: [
{ name: "text", required: true, description: "The text to translate" }, { name: "text", required: true, description: "The text to translate" },
{ {
@ -91,3 +78,13 @@ export const AllFilled: Story = {
}, },
}, },
}; };
export const NoDescription: Story = {
args: {
name: "code-review",
arguments: [
{ name: "code", required: true, description: "The code to review" },
],
argumentValues: {},
},
};

View file

@ -1,4 +1,4 @@
import { Button, Select, Stack, Text, TextInput, Title } from "@mantine/core"; import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core";
export interface PromptArgument { export interface PromptArgument {
name: string; name: string;
@ -6,17 +6,11 @@ export interface PromptArgument {
description?: string; description?: string;
} }
export interface PromptInfo { export interface PromptArgumentsFormProps {
name: string; name: string;
description?: string; description?: string;
}
export interface PromptArgumentsFormProps {
prompts: PromptInfo[];
selectedPrompt?: string;
arguments: PromptArgument[]; arguments: PromptArgument[];
argumentValues: Record<string, string>; argumentValues: Record<string, string>;
onSelectPrompt: (name: string) => void;
onArgumentChange: (name: string, value: string) => void; onArgumentChange: (name: string, value: string) => void;
onGetPrompt: () => void; onGetPrompt: () => void;
} }
@ -31,34 +25,19 @@ function formatPlaceholder(name: string): string {
} }
export function PromptArgumentsForm({ export function PromptArgumentsForm({
prompts, name,
selectedPrompt, description,
arguments: promptArguments, arguments: promptArguments,
argumentValues, argumentValues,
onSelectPrompt,
onArgumentChange, onArgumentChange,
onGetPrompt, onGetPrompt,
}: PromptArgumentsFormProps) { }: PromptArgumentsFormProps) {
const selectedPromptInfo = prompts.find((p) => p.name === selectedPrompt);
return ( return (
<Stack gap="md"> <Stack gap="md">
<Title order={4}>Prompts</Title> <Title order={4}>{name}</Title>
<Select {description && <DescriptionText>{description}</DescriptionText>}
data={prompts.map((p) => ({ value: p.name, label: p.name }))}
value={selectedPrompt ?? null}
onChange={(value) => {
if (value) {
onSelectPrompt(value);
}
}}
placeholder="Choose a prompt..."
/>
{selectedPrompt && selectedPromptInfo?.description && (
<DescriptionText>{selectedPromptInfo.description}</DescriptionText>
)}
{promptArguments.length > 0 && ( {promptArguments.length > 0 && (
<> <Stack gap="sm">
{promptArguments.map((arg) => ( {promptArguments.map((arg) => (
<TextInput <TextInput
key={arg.name} key={arg.name}
@ -72,11 +51,13 @@ export function PromptArgumentsForm({
} }
/> />
))} ))}
</> </Stack>
)} )}
<Button fullWidth onClick={onGetPrompt} disabled={!selectedPrompt}> <Group justify="flex-end">
Get Prompt <Button size="sm" onClick={onGetPrompt}>
</Button> Get Prompt
</Button>
</Group>
</Stack> </Stack>
); );
} }

View file

@ -0,0 +1,76 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { PromptControls } from "./PromptControls";
const meta: Meta<typeof PromptControls> = {
title: "Groups/PromptControls",
component: PromptControls,
args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectPrompt: fn(),
listChanged: false,
},
};
export default meta;
type Story = StoryObj<typeof PromptControls>;
const samplePrompts = [
{
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
{
name: "translate",
description: "Translate text from one language to another",
selected: false,
},
{
name: "analyze",
description: "Analyze sentiment and tone of the text",
selected: false,
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
{ name: "refactor", selected: false },
];
export const Default: Story = {
args: {
prompts: samplePrompts,
},
};
export const WithSelection: Story = {
args: {
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
},
};
export const WithSearch: Story = {
args: {
prompts: samplePrompts,
searchText: "sum",
},
};
export const ListChanged: Story = {
args: {
prompts: samplePrompts,
listChanged: true,
},
};
export const Empty: Story = {
args: {
prompts: [],
},
};

View file

@ -0,0 +1,55 @@
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { PromptListItem } from "../PromptListItem/PromptListItem";
import type { PromptItem } from "../../screens/PromptsScreen/PromptsScreen";
export interface PromptControlsProps {
prompts: PromptItem[];
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
}
function listMaxHeight(): string {
return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
}
export function PromptControls({
prompts,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectPrompt,
}: PromptControlsProps) {
const filteredPrompts = prompts.filter((p) =>
p.name.toLowerCase().includes(searchText.toLowerCase()),
);
return (
<Stack gap="sm">
<Title order={4}>Prompts</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
<TextInput
placeholder="Search prompts..."
value={searchText}
onChange={(e) => onSearchChange(e.currentTarget.value)}
/>
<ScrollArea.Autosize mah={listMaxHeight()}>
<Stack gap="xs">
{filteredPrompts.map((prompt) => (
<PromptListItem
key={prompt.name}
name={prompt.name}
description={prompt.description}
selected={prompt.selected}
onClick={() => onSelectPrompt(prompt.name)}
/>
))}
</Stack>
</ScrollArea.Autosize>
</Stack>
);
}

View file

@ -0,0 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { PromptListItem } from "./PromptListItem";
const meta: Meta<typeof PromptListItem> = {
title: "Groups/PromptListItem",
component: PromptListItem,
args: {
onClick: fn(),
},
};
export default meta;
type Story = StoryObj<typeof PromptListItem>;
export const Default: Story = {
args: {
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
};
export const Selected: Story = {
args: {
name: "translate",
description: "Translate text from one language to another",
selected: true,
},
};
export const NoDescription: Story = {
args: {
name: "code-review",
selected: false,
},
};

View file

@ -0,0 +1,40 @@
import { Stack, Text, UnstyledButton } from "@mantine/core";
export interface PromptListItemProps {
name: string;
description?: string;
selected: boolean;
onClick: () => void;
}
const NameText = Text.withProps({
fw: 500,
});
const DescriptionText = Text.withProps({
size: "xs",
c: "dimmed",
lineClamp: 1,
});
export function PromptListItem({
name,
description,
selected,
onClick,
}: PromptListItemProps) {
return (
<UnstyledButton
w="100%"
p="sm"
variant="listItem"
bg={selected ? "var(--mantine-primary-color-light)" : undefined}
onClick={onClick}
>
<Stack gap={2}>
<NameText>{name}</NameText>
{description && <DescriptionText>{description}</DescriptionText>}
</Stack>
</UnstyledButton>
);
}

View file

@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ToolControls } from "./ToolControls";
const meta: Meta<typeof ToolControls> = {
title: "Groups/ToolControls",
component: ToolControls,
args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(),
onSelectTool: fn(),
listChanged: false,
},
};
export default meta;
type Story = StoryObj<typeof ToolControls>;
const sampleTools = [
{
name: "send_message",
title: "Send Message",
selected: false,
onClick: fn(),
},
{
name: "create_record",
title: "Create Record",
selected: false,
onClick: fn(),
},
{ name: "delete_records", selected: false, onClick: fn() },
{ name: "list_users", selected: false, onClick: fn() },
{ name: "batch_process", selected: false, onClick: fn() },
];
export const Default: Story = {
args: {
tools: sampleTools,
},
};
export const WithSelection: Story = {
args: {
tools: sampleTools.map((t) =>
t.name === "create_record" ? { ...t, selected: true } : t,
),
},
};
export const WithSearch: Story = {
args: {
tools: sampleTools,
searchText: "send",
},
};
export const ListChanged: Story = {
args: {
tools: sampleTools,
listChanged: true,
},
};
export const Empty: Story = {
args: {
tools: [],
},
};

View file

@ -0,0 +1,55 @@
import { ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../ToolListItem/ToolListItem";
import type { ToolListItemProps } from "../ToolListItem/ToolListItem";
export interface ToolControlsProps {
tools: ToolListItemProps[];
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectTool: (name: string) => void;
}
function listMaxHeight(): string {
return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
}
export function ToolControls({
tools,
listChanged,
searchText,
onSearchChange,
onRefreshList,
onSelectTool,
}: ToolControlsProps) {
const filteredTools = searchText
? tools.filter((tool) =>
tool.name.toLowerCase().includes(searchText.toLowerCase()),
)
: tools;
return (
<Stack gap="sm">
<Title order={4}>Tools</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
<TextInput
placeholder="Search tools..."
value={searchText}
onChange={(e) => onSearchChange(e.currentTarget.value)}
/>
<ScrollArea.Autosize mah={listMaxHeight()}>
<Stack gap="xs">
{filteredTools.map((tool) => (
<ToolListItem
key={tool.name}
{...tool}
onClick={() => onSelectTool(tool.name)}
/>
))}
</Stack>
</ScrollArea.Autosize>
</Stack>
);
}

View file

@ -19,7 +19,7 @@ const ScreenLayout = Flex.withProps({
}); });
const Sidebar = Stack.withProps({ const Sidebar = Stack.withProps({
w: 280, w: 340,
flex: "0 0 auto", flex: "0 0 auto",
}); });

View file

@ -34,7 +34,7 @@ const ScreenLayout = Flex.withProps({
}); });
const Sidebar = Stack.withProps({ const Sidebar = Stack.withProps({
w: 280, w: 340,
flex: "0 0 auto", flex: "0 0 auto",
}); });
@ -82,8 +82,6 @@ export function LoggingScreen({
onFilterChange={setFilterText} onFilterChange={setFilterText}
onToggleLevel={handleToggleLevel} onToggleLevel={handleToggleLevel}
onToggleAllLevels={handleToggleAllLevels} onToggleAllLevels={handleToggleAllLevels}
onClear={onClear}
onExport={onExport}
/> />
</SidebarCard> </SidebarCard>
</Sidebar> </Sidebar>
@ -94,6 +92,8 @@ export function LoggingScreen({
autoScroll={autoScroll} autoScroll={autoScroll}
onToggleAutoScroll={onToggleAutoScroll} onToggleAutoScroll={onToggleAutoScroll}
onCopyAll={onCopyAll} onCopyAll={onCopyAll}
onClear={onClear}
onExport={onExport}
/> />
</ScreenLayout> </ScreenLayout>
); );

View file

@ -1,69 +1,90 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { PromptsScreen } from "./PromptsScreen"; import { PromptsScreen } from "./PromptsScreen";
import type { PromptItem, SelectedPrompt } from "./PromptsScreen";
const meta: Meta<typeof PromptsScreen> = { const meta: Meta<typeof PromptsScreen> = {
title: "Screens/PromptsScreen", title: "Screens/PromptsScreen",
component: PromptsScreen, component: PromptsScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
args: { args: {
searchText: "",
onSearchChange: fn(),
onRefreshList: fn(), onRefreshList: fn(),
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
listChanged: false,
}, },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof PromptsScreen>; type Story = StoryObj<typeof PromptsScreen>;
const samplePrompts: PromptItem[] = [
{
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
{
name: "translate",
description: "Translate text from one language to another",
selected: false,
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
{
name: "analyze",
description: "Analyze sentiment and tone of the text",
selected: false,
},
{ name: "refactor", selected: false },
];
const selectedTranslate: SelectedPrompt = {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: {},
};
export const NoSelection: Story = { export const NoSelection: Story = {
args: { args: {
promptForm: { prompts: samplePrompts,
prompts: [ },
{ name: "summarize", description: "Summarize a document" }, };
{
name: "translate", export const PromptSelected: Story = {
description: "Translate text to another language", args: {
}, prompts: samplePrompts.map((p) =>
{ name: "code-review", description: "Review code for issues" }, p.name === "translate" ? { ...p, selected: true } : p,
], ),
selectedPrompt: undefined, selectedPrompt: selectedTranslate,
arguments: [],
argumentValues: {},
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
},
messages: undefined,
listChanged: false,
}, },
}; };
export const WithResult: Story = { export const WithResult: Story = {
args: { args: {
promptForm: { prompts: samplePrompts.map((p) =>
prompts: [ p.name === "translate" ? { ...p, selected: true } : p,
{ name: "summarize", description: "Summarize a document" }, ),
{ selectedPrompt: {
name: "translate", ...selectedTranslate,
description: "Translate text to another language",
},
{ name: "code-review", description: "Review code for issues" },
],
selectedPrompt: "translate",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: { argumentValues: {
text: "Hello, how are you?", text: "Hello, how are you?",
targetLanguage: "es", targetLanguage: "es",
}, },
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
}, },
messages: { messages: {
onCopyAll: fn(), onCopyAll: fn(),
@ -79,6 +100,18 @@ export const WithResult: Story = {
}, },
], ],
}, },
listChanged: false, },
};
export const ListChanged: Story = {
args: {
prompts: samplePrompts,
listChanged: true,
},
};
export const Empty: Story = {
args: {
prompts: [],
}, },
}; };

View file

@ -1,26 +1,56 @@
import { Card, Container, Grid, Stack, Text } from "@mantine/core"; import { Card, Flex, ScrollArea, Stack, Text } from "@mantine/core";
import { PromptControls } from "../../groups/PromptControls/PromptControls";
import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm"; import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay"; import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator"; import type { PromptArgument } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import type { PromptArgumentsFormProps } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay"; import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
export interface PromptsScreenProps { export interface PromptItem {
promptForm: PromptArgumentsFormProps; name: string;
messages?: PromptMessagesDisplayProps; description?: string;
listChanged: boolean; selected: boolean;
onRefreshList: () => void;
} }
const PageContainer = Container.withProps({ export interface SelectedPrompt {
size: "xl", name: string;
py: "xl", description?: string;
arguments: PromptArgument[];
argumentValues: Record<string, string>;
}
export interface PromptsScreenProps {
prompts: PromptItem[];
selectedPrompt?: SelectedPrompt;
messages?: PromptMessagesDisplayProps;
listChanged: boolean;
searchText: string;
onSearchChange: (text: string) => void;
onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
onArgumentChange: (name: string, value: string) => void;
onGetPrompt: () => void;
}
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
}); });
const FullHeightCard = Card.withProps({ const Sidebar = Stack.withProps({
w: 340,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
padding: "lg",
});
const DetailCard = Card.withProps({
withBorder: true, withBorder: true,
padding: "lg", padding: "lg",
h: "100%",
}); });
const EmptyState = Text.withProps({ const EmptyState = Text.withProps({
@ -30,37 +60,62 @@ const EmptyState = Text.withProps({
}); });
export function PromptsScreen({ export function PromptsScreen({
promptForm, prompts,
selectedPrompt,
messages, messages,
listChanged, listChanged,
searchText,
onSearchChange,
onRefreshList, onRefreshList,
onSelectPrompt,
onArgumentChange,
onGetPrompt,
}: PromptsScreenProps) { }: PromptsScreenProps) {
return ( return (
<PageContainer> <ScreenLayout>
<Grid align="stretch"> <Sidebar>
<Grid.Col span={5}> <SidebarCard>
<FullHeightCard> <PromptControls
<Stack gap="md"> prompts={prompts}
<ListChangedIndicator listChanged={listChanged}
visible={listChanged} searchText={searchText}
onRefresh={onRefreshList} onSearchChange={onSearchChange}
/> onRefreshList={onRefreshList}
<PromptArgumentsForm {...promptForm} /> onSelectPrompt={onSelectPrompt}
</Stack> />
</FullHeightCard> </SidebarCard>
</Grid.Col> </Sidebar>
<Grid.Col span={7}>
<FullHeightCard> <ScrollArea.Autosize
{messages ? ( flex={1}
<PromptMessagesDisplay {...messages} /> mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
) : ( >
<EmptyState> <Stack gap="md">
Select a prompt and click Get Prompt to see messages {selectedPrompt ? (
</EmptyState> <>
)} <DetailCard>
</FullHeightCard> <PromptArgumentsForm
</Grid.Col> name={selectedPrompt.name}
</Grid> description={selectedPrompt.description}
</PageContainer> arguments={selectedPrompt.arguments}
argumentValues={selectedPrompt.argumentValues}
onArgumentChange={onArgumentChange}
onGetPrompt={onGetPrompt}
/>
</DetailCard>
{messages && (
<DetailCard>
<PromptMessagesDisplay {...messages} />
</DetailCard>
)}
</>
) : (
<DetailCard>
<EmptyState>Select a prompt to view details</EmptyState>
</DetailCard>
)}
</Stack>
</ScrollArea.Autosize>
</ScreenLayout>
); );
} }

View file

@ -18,7 +18,7 @@ const ScreenLayout = Flex.withProps({
}); });
const Sidebar = Stack.withProps({ const Sidebar = Stack.withProps({
w: 280, w: 340,
flex: "0 0 auto", flex: "0 0 auto",
}); });

View file

@ -1,14 +1,5 @@
import { import { Card, Flex, Grid, Stack, Text } from "@mantine/core";
Card, import { ToolControls } from "../../groups/ToolControls/ToolControls";
Container,
Grid,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../../groups/ToolListItem/ToolListItem";
import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel"; import { ToolDetailPanel } from "../../groups/ToolDetailPanel/ToolDetailPanel";
import { ResultPanel } from "../../groups/ResultPanel/ResultPanel"; import { ResultPanel } from "../../groups/ResultPanel/ResultPanel";
import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem"; import type { ToolListItemProps } from "../../groups/ToolListItem/ToolListItem";
@ -26,9 +17,21 @@ export interface ToolsScreenProps {
onSelectTool: (name: string) => void; onSelectTool: (name: string) => void;
} }
const PageContainer = Container.withProps({ const ScreenLayout = Flex.withProps({
size: "xl", variant: "screen",
py: "xl", h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
});
const Sidebar = Stack.withProps({
w: 340,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
padding: "lg",
}); });
const FullHeightCard = Card.withProps({ const FullHeightCard = Card.withProps({
@ -53,40 +56,23 @@ export function ToolsScreen({
onRefreshList, onRefreshList,
onSelectTool, onSelectTool,
}: ToolsScreenProps) { }: ToolsScreenProps) {
const filteredTools = searchText
? tools.filter((tool) =>
tool.name.toLowerCase().includes(searchText.toLowerCase()),
)
: tools;
return ( return (
<PageContainer> <ScreenLayout>
<Grid align="stretch"> <Sidebar>
<Grid.Col span={3}> <SidebarCard>
<FullHeightCard> <ToolControls
<Stack gap="sm"> tools={tools}
<Title order={4}>Tools</Title> listChanged={listChanged}
<ListChangedIndicator searchText={searchText}
visible={listChanged} onSearchChange={onSearchChange}
onRefresh={onRefreshList} onRefreshList={onRefreshList}
/> onSelectTool={onSelectTool}
<TextInput />
placeholder="Search tools..." </SidebarCard>
value={searchText} </Sidebar>
onChange={(event) => onSearchChange(event.currentTarget.value)}
/>
{filteredTools.map((tool) => (
<ToolListItem
key={tool.name}
{...tool}
onClick={() => onSelectTool(tool.name)}
/>
))}
</Stack>
</FullHeightCard>
</Grid.Col>
<Grid.Col span={5}> <Grid align="stretch" flex={1}>
<Grid.Col span={7}>
<FullHeightCard> <FullHeightCard>
{selectedTool ? ( {selectedTool ? (
<ToolDetailPanel {...selectedTool} /> <ToolDetailPanel {...selectedTool} />
@ -96,7 +82,7 @@ export function ToolsScreen({
</FullHeightCard> </FullHeightCard>
</Grid.Col> </Grid.Col>
<Grid.Col span={4}> <Grid.Col span={5}>
<FullHeightCard> <FullHeightCard>
{result ? ( {result ? (
<ResultPanel {...result} /> <ResultPanel {...result} />
@ -106,6 +92,6 @@ export function ToolsScreen({
</FullHeightCard> </FullHeightCard>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</PageContainer> </ScreenLayout>
); );
} }

View file

@ -465,16 +465,26 @@ export const PromptsActive: Story = {
activeTab: "Prompts", activeTab: "Prompts",
children: ( children: (
<PromptsScreen <PromptsScreen
promptForm={{ prompts={[
prompts: [ {
{ name: "summarize", description: "Summarize a document" }, name: "summarize",
{ description: "Summarize a document",
name: "translate", selected: false,
description: "Translate text to another language", },
}, {
{ name: "code-review", description: "Review code for issues" }, name: "translate",
], description: "Translate text to another language",
selectedPrompt: "translate", selected: true,
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
]}
selectedPrompt={{
name: "translate",
description: "Translate text to another language",
arguments: [ arguments: [
{ {
name: "text", name: "text",
@ -491,9 +501,6 @@ export const PromptsActive: Story = {
text: "Hello, how are you?", text: "Hello, how are you?",
targetLanguage: "es", targetLanguage: "es",
}, },
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
}} }}
messages={{ messages={{
onCopyAll: fn(), onCopyAll: fn(),
@ -507,7 +514,12 @@ export const PromptsActive: Story = {
], ],
}} }}
listChanged={false} listChanged={false}
searchText=""
onSearchChange={fn()}
onRefreshList={fn()} onRefreshList={fn()}
onSelectPrompt={fn()}
onArgumentChange={fn()}
onGetPrompt={fn()}
/> />
), ),
}, },