make buttons

This commit is contained in:
Nate Kelley 2025-09-29 14:26:02 -06:00
parent 4215bd2868
commit dbe8b79ce7
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 284 additions and 6 deletions

View File

@ -0,0 +1,231 @@
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { BusterChatInputBase } from './BusterChatInputBase';
const DEFAULT_USER_SUGGESTED_PROMPTS = {
suggestedPrompts: {
report: [
'provide a trend analysis of quarterly profits',
'evaluate product performance across regions',
],
dashboard: ['create a sales performance dashboard', 'design a revenue forecast dashboard'],
visualization: ['create a metric for monthly sales', 'show top vendors by purchase volume'],
help: [
'what types of analyses can you perform?',
'what questions can I as buster?',
'what data models are available for queries?',
'can you explain your forecasting capabilities?',
],
},
updatedAt: new Date().toISOString(),
};
const meta: Meta<typeof BusterChatInputBase> = {
title: 'Features/Input/BusterChatInputBase',
component: BusterChatInputBase,
decorators: [
(Story) => (
<div style={{ width: '600px', minHeight: '400px', padding: '20px' }}>
<Story />
</div>
),
],
parameters: {
layout: 'centered',
docs: {
description: {
component:
'Chat input component with intelligent suggestions and shortcuts for Buster chat interface. Displays 4 unique random suggestions from suggested prompts plus available shortcuts.',
},
},
},
};
export default meta;
type Story = StoryObj<typeof BusterChatInputBase>;
// Mock shortcuts data
const mockShortcuts: ListShortcutsResponse['shortcuts'] = [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'Weekly Sales Report',
instructions: 'Generate a comprehensive weekly sales report with key metrics and trends',
createdBy: '123e4567-e89b-12d3-a456-426614174001',
updatedBy: null,
organizationId: '123e4567-e89b-12d3-a456-426614174002',
shareWithWorkspace: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
id: '123e4567-e89b-12d3-a456-426614174003',
name: 'Customer Analysis',
instructions: 'Analyze customer behavior patterns and provide insights',
createdBy: '123e4567-e89b-12d3-a456-426614174001',
updatedBy: null,
organizationId: '123e4567-e89b-12d3-a456-426614174002',
shareWithWorkspace: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
{
id: '123e4567-e89b-12d3-a456-426614174004',
name: 'Revenue Forecast',
instructions: 'Create a revenue forecast for the next quarter based on current trends',
createdBy: '123e4567-e89b-12d3-a456-426614174001',
updatedBy: null,
organizationId: '123e4567-e89b-12d3-a456-426614174002',
shareWithWorkspace: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
},
];
export const Default: Story = {
args: {
defaultValue: '',
onSubmit: fn(),
onStop: fn(),
submitting: false,
disabled: false,
shortcuts: mockShortcuts,
suggestedPrompts: DEFAULT_USER_SUGGESTED_PROMPTS.suggestedPrompts,
},
parameters: {
docs: {
description: {
story:
'Default chat input with suggestions from multiple categories (report, dashboard, visualization, help) and shortcuts. Shows 4 unique random suggestions plus available shortcuts with a separator.',
},
},
},
};
export const WithPrefilledText: Story = {
args: {
defaultValue: 'Generate a comprehensive sales report with quarterly trends',
onSubmit: fn(),
onStop: fn(),
submitting: false,
disabled: false,
shortcuts: mockShortcuts,
suggestedPrompts: DEFAULT_USER_SUGGESTED_PROMPTS.suggestedPrompts,
},
parameters: {
docs: {
description: {
story: 'Chat input with pre-filled text to show how default values are handled.',
},
},
},
};
export const Submitting: Story = {
args: {
defaultValue: 'Generate a sales report for Q4',
onSubmit: fn(),
onStop: fn(),
submitting: true,
disabled: false,
shortcuts: mockShortcuts,
suggestedPrompts: DEFAULT_USER_SUGGESTED_PROMPTS.suggestedPrompts,
},
parameters: {
docs: {
description: {
story: 'Chat input in submitting state - shows the state when a query is being processed.',
},
},
},
};
export const Disabled: Story = {
args: {
defaultValue: 'This input is disabled',
onSubmit: fn(),
onStop: fn(),
submitting: false,
disabled: true,
shortcuts: mockShortcuts,
suggestedPrompts: DEFAULT_USER_SUGGESTED_PROMPTS.suggestedPrompts,
},
parameters: {
docs: {
description: {
story: 'Disabled chat input state.',
},
},
},
};
export const NoShortcuts: Story = {
args: {
defaultValue: '',
onSubmit: fn(),
onStop: fn(),
submitting: false,
disabled: false,
shortcuts: [],
suggestedPrompts: DEFAULT_USER_SUGGESTED_PROMPTS.suggestedPrompts,
},
parameters: {
docs: {
description: {
story: 'Chat input with only suggested prompts, no shortcuts available.',
},
},
},
};
export const CustomPrompts: Story = {
args: {
defaultValue: '',
onSubmit: fn(),
onStop: fn(),
submitting: false,
disabled: false,
shortcuts: mockShortcuts,
suggestedPrompts: {
report: [
'Create a detailed financial summary for stakeholders',
'Generate monthly performance metrics report',
'Analyze year-over-year growth trends',
'Compile customer satisfaction survey results',
'Prepare executive dashboard summary',
],
dashboard: [
'Build an interactive sales performance dashboard',
'Create a real-time inventory monitoring dashboard',
'Design customer analytics dashboard',
'Set up operational KPI dashboard',
'Develop marketing campaign performance dashboard',
],
visualization: [
'Show revenue trends by product category',
'Create geographic sales distribution map',
'Visualize customer journey funnel',
'Display seasonal demand patterns',
'Chart employee productivity metrics',
],
help: [
'What data visualization options are available?',
'How do I create custom metrics?',
'What are the best practices for dashboard design?',
'Can you explain predictive analytics features?',
'How do I share reports with my team?',
],
},
},
parameters: {
docs: {
description: {
story:
'Chat input with custom suggested prompts - demonstrates how the component handles larger sets of suggestions (ensures 4 unique samples are selected).',
},
},
},
};

View File

@ -1,12 +1,12 @@
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts'; import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user'; import type { GetSuggestedPromptsResponse } from '@buster/server-shared/user';
import sample from 'lodash/sample';
import sampleSize from 'lodash/sampleSize'; import sampleSize from 'lodash/sampleSize';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput'; import type { MentionSuggestionExtension } from '@/components/ui/inputs/MentionInput';
import type { MentionInputSuggestionsProps } from '@/components/ui/inputs/MentionInputSuggestions'; import type { MentionInputSuggestionsProps } from '@/components/ui/inputs/MentionInputSuggestions';
import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions'; import { MentionInputSuggestions } from '@/components/ui/inputs/MentionInputSuggestions';
import { useMemoizedFn } from '@/hooks/useMemoizedFn'; import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { BusterChatInputButtons } from './BusterChatInputButtons';
export type BusterChatInput = { export type BusterChatInput = {
defaultValue: string; defaultValue: string;
@ -71,7 +71,17 @@ export const BusterChatInputBase: React.FC<BusterChatInput> = React.memo(
onPressEnter={onPressEnter} onPressEnter={onPressEnter}
mentions={mentions} mentions={mentions}
suggestionItems={suggestionItems} suggestionItems={suggestionItems}
/> placeholder="Ask a question or type / for shortcuts..."
>
<BusterChatInputButtons
onSubmit={() => {}}
onStop={() => {}}
submitting={submitting}
disabled={disabled}
mode="auto"
onModeChange={() => {}}
/>
</MentionInputSuggestions>
); );
} }
); );
@ -103,7 +113,7 @@ const useUniqueSuggestions = (
const items: MentionInputSuggestionsProps['suggestionItems'] = fourUniqueSuggestions.map( const items: MentionInputSuggestionsProps['suggestionItems'] = fourUniqueSuggestions.map(
(suggestion) => ({ (suggestion) => ({
type: 'item', type: 'item',
value: suggestion.type, value: suggestion.type + suggestion.value,
label: suggestion.value, label: suggestion.value,
}) })
); );

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Magnifier, Sparkle2 } from '@/components/ui/icons';
import Atom from '@/components/ui/icons/NucleoIconOutlined/atom';
import { AppSegmented, type AppSegmentedProps } from '@/components/ui/segmented';
export type BusterChatInputMode = 'auto' | 'research' | 'deep-research';
type BusterChatInputButtons = {
onSubmit: () => void;
onStop: () => void;
submitting: boolean;
disabled: boolean;
mode: BusterChatInputMode;
onModeChange: (mode: BusterChatInputMode) => void;
};
const modesOptions: AppSegmentedProps<BusterChatInputMode>['options'] = [
{ icon: <Sparkle2 />, value: 'auto' },
{ icon: <Magnifier />, value: 'research' },
{ icon: <Atom />, value: 'deep-research' },
];
export const BusterChatInputButtons = ({
onSubmit,
onStop,
submitting,
disabled,
mode,
onModeChange,
}: BusterChatInputButtons) => {
return (
<div className="flex justify-between items-center gap-2">
<AppSegmented value={mode} options={modesOptions} onChange={(v) => onModeChange(v.value)} />
</div>
);
};

View File

@ -43,11 +43,12 @@ export const MentionInputSuggestions = ({
}: MentionInputSuggestionsProps) => { }: MentionInputSuggestionsProps) => {
const [hasClickedSelect, setHasClickedSelect] = useState(false); const [hasClickedSelect, setHasClickedSelect] = useState(false);
const [value, setValue] = useState(valueProp ?? defaultValue); const [value, setValue] = useState(valueProp ?? defaultValue);
const [hasResults, setHasResults] = useState(false); const [hasResults, setHasResults] = useState(!!suggestionItems.length);
const commandListNavigatedRef = useRef(false); const commandListNavigatedRef = useRef(false);
const commandRef = useRef<HTMLDivElement>(null); const commandRef = useRef<HTMLDivElement>(null);
const mentionsInputRef = useRef<MentionInputRef>(null); const mentionsInputRef = useRef<MentionInputRef>(null);
console.log(hasResults);
const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0; const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0;
@ -140,7 +141,7 @@ export const MentionInputSuggestions = ({
commandListNavigatedRef={commandListNavigatedRef} commandListNavigatedRef={commandListNavigatedRef}
disabled={disabled} disabled={disabled}
/> />
{children} {children && <div className="mt-3">{children}</div>}
</MentionInputSuggestionsContainer> </MentionInputSuggestionsContainer>
{hasResults && <div className="border-b mb-1.5" />} {hasResults && <div className="border-b mb-1.5" />}
<MentionInputSuggestionsList <MentionInputSuggestionsList

View File

@ -7,7 +7,7 @@ export const MentionInputSuggestionsSeparator = ({
...props ...props
}: React.ComponentPropsWithoutRef<typeof Command.Separator>) => { }: React.ComponentPropsWithoutRef<typeof Command.Separator>) => {
return ( return (
<Command.Separator className={cn('bg-border -mx-1 h-px', props.className)} {...props}> <Command.Separator className={cn('bg-border -mx-1 h-px my-1.5', props.className)} {...props}>
{children} {children}
</Command.Separator> </Command.Separator>
); );