Beam: change the Fusion model

This commit is contained in:
Enrico Ros
2024-03-26 17:36:02 -07:00
parent aeb1acf458
commit 274f11ef1d
18 changed files with 786 additions and 494 deletions
+36
View File
@@ -11,6 +11,7 @@ export const beamCardClasses = {
errored: 'beamCard-Errored',
selectable: 'beamCard-Selectable',
attractive: 'beamCard-Attractive',
smashTop: 'beamCard-SmashTop',
};
/**
@@ -43,6 +44,11 @@ export const BeamCard = styled(Box)(({ theme }) => ({
[`&.${beamCardClasses.attractive}`]: {
animation: `${animationShadowLimey} 2s linear infinite`,
},
[`&.${beamCardClasses.smashTop}`]: {
borderTop: 'none',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
position: 'relative',
@@ -56,6 +62,36 @@ export const BeamCard = styled(Box)(({ theme }) => ({
}));
BeamCard.displayName = 'BeamCard'; // [shared] scatter/gather pane style
export const beamCardMessageWrapperSx: SxProps = {
minHeight: '1.5rem',
display: 'flex',
flexDirection: 'column',
// uncomment the following to limit the message height
// overflow: 'auto',
// maxHeight: 'calc(0.8 * (100vh - 16rem))',
// aspectRatio: 1,
}
export const beamCardMessageSx: SxProps = {
// style: to undo the style of ChatMessage
backgroundColor: 'none',
border: 'none',
mx: -1.5, // compensates for the marging (e.g. RenderChatText, )
my: 0,
px: 0,
py: 0,
}
export const beamCardMessageScrollingSx: SxProps = {
...beamCardMessageSx,
overflow: 'auto',
maxHeight: 'max(18rem, calc(60lvh - 16rem))',
}
/**
* Props for the two panes.
*/
+75 -26
View File
@@ -1,14 +1,15 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Alert, Box } from '@mui/joy';
import { Alert, Box, CircularProgress } from '@mui/joy';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { animationEnterScaleUp } from '~/common/util/animUtils';
import { useUICounter } from '~/common/state/store-ui';
import { BeamExplainer } from './BeamExplainer';
import { BeamGatherInput } from './gather/BeamGatherInput';
import { BeamGatherOutput } from './gather/BeamGatherOutput';
import { BeamFusionGrid } from './gather/BeamFusionGrid';
import { BeamGatherPane } from './gather/BeamGatherPane';
import { BeamRayGrid } from './scatter/BeamRayGrid';
import { BeamScatterInput } from './scatter/BeamScatterInput';
@@ -23,6 +24,9 @@ export function BeamView(props: {
showExplainer?: boolean,
}) {
// state
const [warnIsScattering, setWarnIsScattering] = React.useState(false);
// external state
const { novel: explainerUnseen, touch: explainerCompleted, forget: explainerShow } = useUICounter('beam-wizard');
const {
@@ -32,12 +36,19 @@ export function BeamView(props: {
const {
/* root */ inputHistory, inputIssues, inputReady,
/* scatter */ isScattering, raysReady,
/* gather (composite) */ canGather,
} = useBeamStore(props.beamStore, useShallow(state => ({
inputHistory: state.inputHistory, inputIssues: state.inputIssues, inputReady: state.inputReady,
isScattering: state.isScattering, raysReady: state.raysReady,
inputHistory: state.inputHistory,
inputIssues: state.inputIssues,
inputReady: state.inputReady,
// scatter
isScattering: state.isScattering,
raysReady: state.raysReady,
// gather (composite)
canGather: state.raysReady >= 2 && state.currentFactoryId !== null && state.currentGatherLlmId !== null,
})));
const rayIds = useBeamStore(props.beamStore, useShallow(state => state.rays.map(ray => ray.rayId)));
// const fusionIds = useBeamStore(props.beamStore, useShallow(state => state.fusions.map(fusion => fusion.fusionId)));
const fusionIds = useBeamStore(props.beamStore, useShallow(state => state.fusions.map(fusion => fusion.fusionId)));
// derived state
const raysCount = rayIds.length;
@@ -50,6 +61,32 @@ export function BeamView(props: {
const handleRayIncreaseCount = React.useCallback(() => setRayCount(raysCount + 1), [setRayCount, raysCount]);
const handleCreateFusion = React.useCallback(() => {
// if scatter is busy, ask for confirmation
if (isScattering) {
setWarnIsScattering(true);
return;
}
props.beamStore.getState().createFusion();
}, [isScattering, props.beamStore]);
const handleStopScatterConfirmation = React.useCallback(() => {
setWarnIsScattering(false);
stopScatteringAll();
handleCreateFusion();
}, [handleCreateFusion, stopScatteringAll]);
const handleStopScatterDenial = React.useCallback(() => setWarnIsScattering(false), []);
// (this is great ux) scatter freed up while we were asking the question, proceed
React.useEffect(() => {
if (warnIsScattering && !isScattering)
handleStopScatterConfirmation();
}, [handleStopScatterConfirmation, isScattering, warnIsScattering]);
// runnning
// [effect] pre-populate a default number of rays
@@ -66,6 +103,7 @@ export function BeamView(props: {
return (
<ScrollToBottom disableAutoStick>
{/* Main V-Layout */}
<Box sx={{
// scroller fill
minHeight: '100%',
@@ -114,43 +152,54 @@ export function BeamView(props: {
isMobile={props.isMobile}
rayIds={rayIds}
onIncreaseRayCount={handleRayIncreaseCount}
// linkedLlmId={lastGatherLlmId}
// linkedLlmId={currentGatherLlmId}
/>
{/* Gapper between Rays and Merge, without compromising the auto margin of the Ray Grid */}
<Box />
{/* Fusion Config */}
<BeamGatherInput
beamStore={props.beamStore}
/>
{/*<BeamFusionGrid*/}
{/* beamStore={props.beamStore}*/}
{/* canAddFusion={raysReady >= 2}*/}
{/* fusionIds={fusionIds}*/}
{/* isMobile={props.isMobile}*/}
{/* onAddFusion={() => alert('add fusion xxx')}*/}
{/* raysCount={raysCount}*/}
{/*/>*/}
{/* Gather Controls */}
<BeamGatherPane
isMobile={props.isMobile}
beamStore={props.beamStore}
gatherCount={raysReady}
scatterBusy={isScattering}
canGather={canGather}
isMobile={props.isMobile}
onAddFusion={handleCreateFusion}
raysReady={raysReady}
/>
{/* Fusion Output */}
<BeamGatherOutput
{/* Fusion Grid */}
<BeamFusionGrid
beamStore={props.beamStore}
canGather={canGather}
fusionIds={fusionIds}
isMobile={props.isMobile}
onAddFusion={handleCreateFusion}
raysCount={raysCount}
/>
</Box>
{/* Confirm Stop Scattering */}
{warnIsScattering && (
<ConfirmationModal
open
onClose={handleStopScatterDenial}
onPositive={handleStopScatterConfirmation}
// lowStakes
noTitleBar
confirmationText='Some responses are still being generated. Do you want to stop and proceed with merging the available responses now?'
positiveActionText='Proceed with Merge'
negativeActionText='Wait for All Responses'
negativeActionStartDecorator={
<CircularProgress color='neutral' sx={{ '--CircularProgress-size': '24px', '--CircularProgress-trackThickness': '1px' }} />
}
/>
)}
</ScrollToBottom>
);
}
-1
View File
@@ -21,6 +21,5 @@ export const SCATTER_RAY_SHOW_DRAG_HANDLE = false;
// BEAM Gather configuration
export const GATHER_COLOR = 'success' as const;
export const GATHER_DEFAULT_TO_FIRST_FUSION = true;
export const GATHER_PLACEHOLDER = '📦 ...';
export const GATHER_SHOW_SYSTEM_PROMPT = false;
+283
View File
@@ -0,0 +1,283 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, SvgIconProps, Typography } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
import RemoveCircleOutlineRoundedIcon from '@mui/icons-material/RemoveCircleOutlineRounded';
import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded';
import StopRoundedIcon from '@mui/icons-material/StopRounded';
import TelegramIcon from '@mui/icons-material/Telegram';
import { ChatMessageMemo } from '../../../apps/chat/components/message/ChatMessage';
import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
import { findLLMOrThrow } from '~/modules/llms/store-llms';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { InlineError } from '~/common/components/InlineError';
import { animationEnterBelow } from '~/common/util/animUtils';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { BFusion, fusionIsError, fusionIsFusing, fusionIsIdle, fusionIsStopped, fusionIsUsableOutput } from './beam.gather';
import { BeamCard, beamCardClasses, beamCardMessageScrollingSx, beamCardMessageSx, beamCardMessageWrapperSx } from '../BeamCard';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { GATHER_COLOR } from '../beam.config';
import { findFusionFactory, FusionFactorySpec } from './instructions/beam.gather.factories';
import { useBeamCardScrolling } from '../store-module-beam';
const fusionCardSx: SxProps = {
// [`&.${beamCardClasses.idle}`]: {
// pb: 0, // Peekaboo (shrink height)
// },
// boxShadow: 'sm',
// borderColor: `${GATHER_COLOR}.outlinedBorder`,
};
const FusionControlsMemo = React.memo(FusionControls);
function FusionControls(props: {
fusion: BFusion,
factory: FusionFactorySpec,
isEmpty: boolean,
isFusing: boolean,
isStopped: boolean,
llmLabel: string,
llmVendorIcon?: React.FunctionComponent<SvgIconProps>,
onRemove: () => void,
onToggleGenerate: () => void,
}) {
return (
<Box
// color='success'
// variant='solid'
// invertedColors
sx={{
// mx: -1, mt: -1, px: 1, py: 0,
borderRadius: 'sm',
display: 'flex', alignItems: 'center', gap: 1,
}}
>
{/* LLM Icon */}
{!!props.llmVendorIcon && (
<GoodTooltip title={props.llmLabel}>
<Box sx={{ display: 'flex' }}>
<props.llmVendorIcon sx={{ fontSize: 'lg', my: 'auto' }} />
</Box>
</GoodTooltip>
)}
{/* Factory Icon */}
{!!props.factory.Icon && (
<props.factory.Icon sx={{ fontSize: 'lg', my: 'auto' }} />
)}
{/* Title / Progress Component */}
<Box sx={{
flex: 1,
// ml: -1, my: -1, pl: 1, py: 0.5, borderRadius: 'md',
display: 'flex',
alignItems: 'center',
}}>
{props.fusion.fusingProgressComponent
// Show the progress in place of the title
? props.fusion.fusingProgressComponent
: (
<Typography sx={{ fontSize: 'sm', fontWeight: 'md' }}>
{props.factory.label + ' Merge'} {props.isStopped && <em> - Interrupted</em>}
</Typography>
)}
</Box>
{!props.isFusing ? (
<GoodTooltip title='Generate'>
<IconButton size='sm' variant='plain' color='success' onClick={props.onToggleGenerate}>
{props.isEmpty ? <PlayArrowRoundedIcon sx={{ fontSize: 'xl2' }} /> : <ReplayRoundedIcon />}
</IconButton>
</GoodTooltip>
) : (
<GoodTooltip title='Stop'>
<IconButton size='sm' variant='plain' color='danger' onClick={props.onToggleGenerate}>
<StopRoundedIcon />
</IconButton>
</GoodTooltip>
)}
<GoodTooltip title='Remove'>
<IconButton size='sm' variant='plain' color='neutral' onClick={props.onRemove}>
<RemoveCircleOutlineRoundedIcon />
</IconButton>
</GoodTooltip>
</Box>
);
}
export function BeamFusion(props: {
beamStore: BeamStoreApi,
fusionId: string,
}) {
// external state
const fusion = useBeamStore(props.beamStore, store => store.fusions.find(fusion => fusion.fusionId === props.fusionId) ?? null);
const cardScrolling = useBeamCardScrolling();
// derived state
const isIdle = fusionIsIdle(fusion);
const isError = fusionIsError(fusion);
const isFusing = fusionIsFusing(fusion);
const isStopped = fusionIsStopped(fusion);
const isUsable = fusionIsUsableOutput(fusion);
const showUseButtons = isUsable && !isFusing;
const factory = findFusionFactory(fusion?.factoryId);
const { removeFusion, toggleFusionGathering } = props.beamStore.getState();
// get LLM Label and Vendor Icon
const llmId = fusion?.llmId ?? null;
const { llmLabel, llmVendorIcon } = React.useMemo(() => {
if (llmId) {
try {
const llm = findLLMOrThrow(llmId);
return {
llmLabel: llm.label,
llmVendorIcon: findVendorById(llm._source?.vId)?.Icon,
};
} catch (e) {
}
}
return { llmLabel: 'Model unknown', llmVendorIcon: undefined };
}, [llmId]);
// handlers
const handleFusionCopy = React.useCallback(() => {
const { fusions } = props.beamStore.getState();
const fusion = fusions.find(fusion => fusion.fusionId === props.fusionId);
if (fusion?.outputDMessage?.text)
copyToClipboard(fusion.outputDMessage.text, 'Merge');
}, [props.beamStore, props.fusionId]);
const handleFusionUse = React.useCallback(() => {
// get snapshot values, so we don't have to react to the hook
const { fusions, onSuccessCallback } = props.beamStore.getState();
const fusion = fusions.find(fusion => fusion.fusionId === props.fusionId);
if (fusion?.outputDMessage?.text && onSuccessCallback)
onSuccessCallback(fusion.outputDMessage.text, fusion.llmId || '');
}, [props.beamStore, props.fusionId]);
const handleFusionRemove = React.useCallback(() => {
removeFusion(props.fusionId);
}, [props.fusionId, removeFusion]);
const handleToggleFusionGather = React.useCallback(() => {
toggleFusionGathering(props.fusionId);
}, [props.fusionId, toggleFusionGathering]);
// escape hatch: no factory, no fusion - nothing to do
if (!fusion || !factory)
return;
return (
<BeamCard
className={
(isIdle ? beamCardClasses.idle : '')
+ (isError ? beamCardClasses.errored + ' ' : '')
+ (isUsable ? beamCardClasses.selectable + ' ' : '')
+ (isFusing ? beamCardClasses.attractive + ' ' : '')
// + (beamCardClasses.smashTop + ' ')
}
>
{/* Controls Row */}
<FusionControlsMemo
fusion={fusion}
factory={factory}
isEmpty={!isUsable}
isFusing={isFusing}
isStopped={isStopped}
llmLabel={llmLabel}
llmVendorIcon={llmVendorIcon}
onRemove={handleFusionRemove}
onToggleGenerate={handleToggleFusionGather}
/>
{/* Show issue, if any */}
{isError && <InlineError error={fusion?.errorText || 'Merge Issue'} />}
{/* Dyanmic: the progress, set by the execution chain */}
{/*{fusion?.fusingProgressComponent && fusion.fusingProgressComponent}*/}
{/* Dynamic: instruction-specific components */}
{!!fusion?.fusingInstructionComponent && fusion.fusingInstructionComponent}
{/* Output Message */}
{(!!fusion?.outputDMessage?.text || fusion?.stage === 'fusing') && (
<Box sx={beamCardMessageWrapperSx}>
{!!fusion.outputDMessage && (
<ChatMessageMemo
message={fusion.outputDMessage}
fitScreen={true}
showAvatar={false}
adjustContentScaling={-1}
sx={!cardScrolling ? beamCardMessageSx : beamCardMessageScrollingSx}
/>
)}
</Box>
)}
{/* Use Fusion */}
{showUseButtons && (
<Box sx={{ mt: 'auto', mb: -1, mr: -1, placeSelf: 'end', display: 'flex', gap: 1 }}>
{/* Copy */}
<GoodTooltip title='Copy'>
<IconButton
onClick={handleFusionCopy}
>
<ContentCopyIcon sx={{ fontSize: 'md' }} />
</IconButton>
</GoodTooltip>
{/* Continue */}
<GoodTooltip title='Use this message'>
<IconButton
size='sm'
// variant='solid'
color={GATHER_COLOR}
disabled={isFusing}
onClick={handleFusionUse}
// endDecorator={<TelegramIcon />}
sx={{
// ...BEAM_BTN_SX,
// fontSize: 'xs',
// backgroundColor: 'background.popup',
// border: '1px solid',
// borderColor: `${GATHER_COLOR}.outlinedBorder`,
// boxShadow: `0 4px 16px -4px rgb(var(--joy-palette-${GATHER_COLOR}-mainChannel) / 20%)`,
animation: `${animationEnterBelow} 0.1s ease-out`,
// whiteSpace: 'nowrap',
}}
>
{/*Ok*/}
<TelegramIcon />
</IconButton>
</GoodTooltip>
</Box>
)}
</BeamCard>
);
}
+160
View File
@@ -0,0 +1,160 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import type { SxProps, VariantProp } from '@mui/joy/styles/types';
import { Box, Button, Typography } from '@mui/joy';
import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
import { BeamFusion } from '~/modules/beam/gather/BeamFusion';
import { findFusionFactory, FusionFactorySpec } from '~/modules/beam/gather/instructions/beam.gather.factories';
import { BeamCard, beamCardClasses } from '../BeamCard';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { GATHER_COLOR } from '../beam.config';
const fusionGridDesktopSx: SxProps = {
mt: 'calc(-1 * var(--Pad))', // absorb parent 'gap' to previous
px: 'var(--Pad)',
pb: 'var(--Pad)',
// backgroundColor: 'neutral.solidBg',
// mb:'auto',
// like rayGridDesktopSx
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(max(min(100%, 390px), 100%/5), 1fr))',
gap: 'var(--Pad)',
} as const;
const fusionGridMobileSx: SxProps = {
...fusionGridDesktopSx,
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
} as const;
export function FusionAddButton(props: {
canGather: boolean,
currentFactory: FusionFactorySpec | null,
onAddFusion: () => void,
sx?: SxProps,
small?: boolean,
textOverride?: string,
variant?: VariantProp,
}) {
if (props.currentFactory === null) return null;
return (
<Button
size={props.small ? 'sm' : undefined}
color={GATHER_COLOR}
variant={props.variant}
disabled={!props.canGather}
onClick={props.onAddFusion}
startDecorator={props.currentFactory?.Icon ? <props.currentFactory.Icon /> : <AddCircleOutlineRoundedIcon />}
sx={{
// justifyContent: 'end',
// gap: 1,
...props.sx,
}}
>
{props.textOverride || props.currentFactory?.addLabel}
</Button>
);
}
export function BeamFusionGrid(props: {
beamStore: BeamStoreApi,
canGather: boolean,
fusionIds: string[],
isMobile: boolean,
onAddFusion: () => void,
raysCount: number,
}) {
// external state
const {
currentFactory,
} = useBeamStore(props.beamStore, useShallow(state => ({
currentFactory: findFusionFactory(state.currentFactoryId),
})));
// derived state
const isEmpty = props.fusionIds.length === 0;
const isNoFactorySelected = currentFactory === null;
// to balance things out with the ray grid, we may need to pad the items
// const targetCount = props.raysCount + 1;
// const fusionCount = props.fusionIds.length + 1;
// // const padItems = targetCount - fusionCount;
// const padItems = 1;
return (
<Box sx={{
...(props.isMobile ? fusionGridMobileSx : fusionGridDesktopSx),
...(isEmpty ? {
backgroundColor: 'neutral.solidBg',
} : {
pt: 'var(--Pad)',
}),
}}>
{/* Fusions */}
{props.fusionIds.map((fusionId) => (
<BeamFusion
key={'fusion-' + fusionId}
beamStore={props.beamStore}
fusionId={fusionId}
/>
))}
{/* Add Fusion (Card) */}
{(isEmpty || !isNoFactorySelected) && (
<BeamCard
className={isEmpty ? beamCardClasses.smashTop : undefined}
sx={{
backgroundColor: props.canGather ? `${GATHER_COLOR}.softBg` : undefined,
// boxShadow: `0px 6px 16px -12px rgb(var(--joy-palette-${props.canGather ? GATHER_COLOR : 'neutral'}-darkChannel) / 40%)`,
mb: 'auto',
}}
>
{isNoFactorySelected ? null : props.canGather ? <Box sx={{ display: 'flex', flexDirection: props.isMobile ? 'column-reverse' : undefined, alignItems: props.isMobile ? undefined : 'center', gap: 1 }}>
<FusionAddButton
// small
// variant='soft'
canGather={props.canGather}
currentFactory={currentFactory}
onAddFusion={props.onAddFusion}
sx={{
minHeight: props.isMobile ? 'calc(2 * var(--Card-padding) + 2rem - 0.5rem)' : undefined,
// marginBottom: 'calc(-1 * var(--Card-padding) + 0.25rem)',
// marginInline: 'calc(-1 * var(--Card-padding) + 0.375rem)',
whiteSpace: 'nowrap',
}}
/>
<Typography level='body-sm' variant='soft' color={GATHER_COLOR}>
{currentFactory.description}
</Typography>
</Box> : (
<Typography level='body-sm'>
You need two or more replies for a {currentFactory?.label?.toLocaleLowerCase() ?? ''} merge.
</Typography>
)}
</BeamCard>
)}
{/* Pad items: N * <Box/> */}
{/*{padItems > 0 && (*/}
{/* Array.from({ length: padItems }).map((_, index) => (*/}
{/* <Box key={'pad-' + index} />*/}
{/* ))*/}
{/*)}*/}
</Box>
);
}
+3 -5
View File
@@ -11,7 +11,6 @@ import type { ChatGenerateInstruction } from './instructions/ChatGenerateInstruc
import type { Instruction } from './instructions/beam.gather.execution';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { GATHER_SHOW_SYSTEM_PROMPT } from '../beam.config';
import { fusionIsEditable } from './beam.gather';
import { useModuleBeamStore } from '../store-module-beam';
@@ -184,11 +183,10 @@ export function BeamGatherInput(props: {
const {
currentFusionId, currentIsEditable, currentInstructions,
} = useBeamStore(props.beamStore, useShallow(store => {
const fusion = store.currentFusionId !== null ? store.fusions.find(fusion => fusion.fusionId === store.currentFusionId) ?? null : null;
return {
currentFusionId: fusion?.fusionId ?? null,
currentIsEditable: fusion ? fusionIsEditable(fusion) : false,
currentInstructions: fusion?.instructions ?? [],
currentFusionId: null, // fusion?.fusionId ?? null,
currentIsEditable: false, // fusion ? fusionIsEditable(fusion) : false,
currentInstructions: [], // fusion?.instructions ?? [],
};
}));
@@ -1,168 +0,0 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, IconButton } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import TelegramIcon from '@mui/icons-material/Telegram';
import { ChatMessageMemo } from '../../../apps/chat/components/message/ChatMessage';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { InlineError } from '~/common/components/InlineError';
import { animationEnterBelow } from '~/common/util/animUtils';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { BEAM_BTN_SX, BEAM_INVERT_BACKGROUND, GATHER_COLOR } from '../beam.config';
import { BeamCard, beamCardClasses } from '../BeamCard';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { fusionIsError, fusionIsFusing, fusionIsIdle, fusionIsUsableOutput } from './beam.gather';
const outputWrapperSx: SxProps = {
mt: 'calc(-1 * var(--Pad))', // absorb parent 'gap' to previous
px: 'var(--Pad)',
pb: 'var(--Pad_2)',
};
const outputWrapperINVSx: SxProps = {
...outputWrapperSx,
backgroundColor: 'neutral.solidBg',
};
const fusionCardSx: SxProps = {
// [`&.${beamCardClasses.idle}`]: {
// pb: 0, // Peekaboo (shrink height)
// },
// boxShadow: 'sm',
// borderColor: `${GATHER_COLOR}.outlinedBorder`,
borderTop: 'none',
// borderRadius: 'sm',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
};
export const fusionChatMessageSx: SxProps = {
// style: to undo the style of ChatMessage
backgroundColor: 'none',
border: 'none',
mx: -1.5, // compensates for the marging (e.g. RenderChatText, )
my: 0,
px: 0,
py: 0,
} as const;
// const placeholderMessage = createDMessage('assistant', 'Click the Merge button to combine the Beams.');
export function BeamGatherOutput(props: {
isMobile: boolean,
beamStore: BeamStoreApi
}) {
// external state - we work on 'currentFusionId'
const { fusion } = useBeamStore(props.beamStore, useShallow(store => {
const fusion = store.currentFusionId !== null ? store.fusions.find(fusion => fusion.fusionId === store.currentFusionId) ?? null : null;
return {
fusion,
};
}));
// derived state
const isIdle = fusionIsIdle(fusion);
const isError = fusionIsError(fusion);
const isFusing = fusionIsFusing(fusion);
const isUsableOutput = fusionIsUsableOutput(fusion);
const showUseOutputButtons = isUsableOutput && !isFusing;
// handlers
const handleFusionCopy = React.useCallback(() => {
const { _currentFusion } = props.beamStore.getState();
const fusion = _currentFusion();
if (fusion?.outputDMessage?.text)
copyToClipboard(fusion.outputDMessage.text, 'Fusion');
}, [props.beamStore]);
const handleFusionUse = React.useCallback(() => {
// get snapshot values, so we don't have to react to the hook
const { _currentFusion, onSuccessCallback, lastGatherLlmId } = props.beamStore.getState();
const fusion = _currentFusion();
if (fusion?.outputDMessage?.text && onSuccessCallback)
onSuccessCallback(fusion.outputDMessage.text, lastGatherLlmId || '');
}, [props.beamStore]);
// if (isIdle)
// return null;
return (
<Box sx={BEAM_INVERT_BACKGROUND ? outputWrapperINVSx : outputWrapperSx}>
<BeamCard
className={`${isIdle ? beamCardClasses.idle : ''} ${showUseOutputButtons ? beamCardClasses.selectable : ''}`}
sx={fusionCardSx}
>
{/* Show issue, if any */}
{isError && <InlineError error={fusion?.errorText || 'Merge Issue'} />}
{/* Dyanmic: the progress, set by the execution chain */}
{fusion?.fusingProgressComponent && fusion.fusingProgressComponent}
{/* Dynamic: instruction-specific components */}
{!!fusion?.fusingInstructionComponent && fusion.fusingInstructionComponent}
{/* Output */}
{!!fusion?.outputDMessage && (
<ChatMessageMemo
message={fusion.outputDMessage}
fitScreen={props.isMobile}
showAvatar={false}
adjustContentScaling={-1}
sx={fusionChatMessageSx}
/>
)}
{/* Use Output */}
{showUseOutputButtons && (
<Box sx={{ mt: 'auto', placeSelf: 'end', display: 'flex', gap: 2 }}>
{/* Copy */}
<GoodTooltip title='Copy'>
<IconButton
onClick={handleFusionCopy}
>
<ContentCopyIcon sx={{ fontSize: 'md' }} />
</IconButton>
</GoodTooltip>
{/* Continue */}
<GoodTooltip title='Use this message'>
<Button
variant='solid' color={GATHER_COLOR}
disabled={isFusing}
onClick={handleFusionUse}
endDecorator={<TelegramIcon />}
sx={{
...BEAM_BTN_SX,
whiteSpace: 'nowrap',
// backgroundColor: 'background.popup',
// border: '1px solid',
// borderColor: `${GATHER_COLOR}.outlinedBorder`,
boxShadow: `0 4px 16px -4px rgb(var(--joy-palette-${GATHER_COLOR}-mainChannel) / 20%)`,
animation: `${animationEnterBelow} 0.1s ease-out`,
}}
>
Ok
</Button>
</GoodTooltip>
</Box>
)}
</BeamCard>
</Box>
);
}
+76 -184
View File
@@ -2,27 +2,19 @@ import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import type { ColorPaletteProp, SxProps } from '@mui/joy/styles/types';
import { Box, Button, ButtonGroup, CircularProgress, FormControl, Typography } from '@mui/joy';
import { Box, Button, ButtonGroup, FormControl, Typography } from '@mui/joy';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import AutoAwesomeMotionTwoToneIcon from '@mui/icons-material/AutoAwesomeMotionTwoTone';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import MergeRoundedIcon from '@mui/icons-material/MergeRounded';
import StopRoundedIcon from '@mui/icons-material/StopRounded';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
import { animationColorBeamGather, animationShadowRingLimey } from '~/common/util/animUtils';
import { animationColorBeamGather } from '~/common/util/animUtils';
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
import { BEAM_BTN_SX, GATHER_COLOR } from '../beam.config';
import { BeamGatherDropdown } from './BeamGatherPaneDropdown';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { FUSION_FACTORIES } from './instructions/beam.gather.factories';
import { FFactoryId, findFusionFactory, FUSION_FACTORIES } from './instructions/beam.gather.factories';
import { GATHER_COLOR } from '../beam.config';
import { beamPaneSx } from '../BeamCard';
import { fusionIsFusing, fusionIsUsableOutput } from './beam.gather';
import { useModuleBeamStore } from '../store-module-beam';
@@ -40,10 +32,10 @@ const gatherPaneSx: SxProps = {
boxShadow: `0px 6px 20px -8px rgb(var(--joy-palette-neutral-darkChannel) / 30%)`,
[`&.${gatherPaneClasses.ready}`]: {
backgroundColor: 'background.popup',
boxShadow: `0px 6px 16px -8px rgb(var(--joy-palette-neutral-darkChannel) / 40%)`,
boxShadow: `0px 6px 16px -8px rgb(var(--joy-palette-success-darkChannel) / 40%)`,
},
[`&.${gatherPaneClasses.busy}`]: {
animation: `${animationShadowRingLimey} 2s linear infinite`,
// animation: `${animationShadowRingLimey} 2s linear infinite`,
},
};
@@ -65,97 +57,52 @@ const desktopGatherPaneSx: SxProps = {
export function BeamGatherPane(props: {
isMobile: boolean,
beamStore: BeamStoreApi,
gatherCount: number,
scatterBusy: boolean,
canGather: boolean,
isMobile: boolean,
onAddFusion: () => void,
raysReady: number,
}) {
// state
const [warnScatterBusy, setWarnScatterBusy] = React.useState(false);
// external state
const { setStickToBottom } = useScrollToBottom();
// const { setStickToBottom } = useScrollToBottom();
const gatherShowDevMethods = useModuleBeamStore(state => state.gatherShowDevMethods);
const {
lastGatherLlmId, fusions, currentFusionId, isGatheringAny, isCurrentFusionGoodToGo,
setLastGatherLlmId, setCurrentFusionId, currentFusionStart, currentFusionStop,
stopScatteringAll,
} = useBeamStore(props.beamStore, useShallow(state => {
const currentFusion = state._currentFusion();
const isCurrentFusionGoodToGo = fusionIsUsableOutput(currentFusion) && !fusionIsFusing(currentFusion);
return {
// state
lastGatherLlmId: state.lastGatherLlmId,
currentFusionId: state.currentFusionId,
fusions: state.fusions,
isGatheringAny: state.isGatheringAny,
isCurrentFusionGoodToGo,
currentFactory, currentFactoryId, currentGatherLlmId, isGatheringAny,
setCurrentFactoryId, setCurrentGatherLlmId,
} = useBeamStore(props.beamStore, useShallow(state => ({
// state
currentFactory: findFusionFactory(state.currentFactoryId),
currentFactoryId: state.currentFactoryId,
currentGatherLlmId: state.currentGatherLlmId,
isGatheringAny: state.isGatheringAny,
// actions
setLastGatherLlmId: state.setLastGatherLlmId,
setCurrentFusionId: state.setCurrentFusionId,
currentFusionStart: state.currentFusionStart,
currentFusionStop: state.currentFusionStop,
// (external slice) scatter actions
stopScatteringAll: state.stopScatteringAll,
};
}));
// actions
setCurrentFactoryId: state.setCurrentFactoryId,
setCurrentGatherLlmId: state.setCurrentGatherLlmId,
})));
const [_, gatherLlmComponent, gatherLlmIcon] = useLLMSelect(
lastGatherLlmId, setLastGatherLlmId, props.isMobile ? '' : 'Merge Model', true,
currentGatherLlmId, setCurrentGatherLlmId, props.isMobile ? '' : 'Merge Model', true,
);
// derived state
const { gatherCount } = props;
const isNoFactorySelected = currentFactoryId === null;
const hasInputs = gatherCount >= 2;
// const CurrentFactoryIcon = currentFactory?.Icon ?? null;
// const currentFactoryDescription = currentFactory?.description ?? '';
const gatherEnabled = hasInputs && !isGatheringAny && currentFusionId !== null;
// const currentFusion = currentFusionId !== null ? fusions.find(fusion => fusion.fusionId === currentFusionId) ?? null : null;
// const currentFactoryId = currentFusion ? currentFusion.factoryId : null;
// const CurrentFusionIcon = currentFactoryId ? FUSION_FACTORIES.find(factory => factory.id === currentFactoryId)?.Icon ?? null : null;
const handleFusionActivate = React.useCallback((fusionId: string, shiftPressed: boolean) => {
setStickToBottom(true);
setCurrentFusionId((fusionId !== currentFusionId || !shiftPressed) ? fusionId : null);
}, [currentFusionId, setCurrentFusionId, setStickToBottom]);
const handleCurrentFusionStart = React.useCallback(() => {
// if scatter is busy, ask for confirmation
if (props.scatterBusy) {
setWarnScatterBusy(true);
return;
}
const { inputHistory, rays } = props.beamStore.getState();
currentFusionStart(inputHistory ? [...inputHistory] : [], rays.map(ray => ray.message));
}, [currentFusionStart, props.beamStore, props.scatterBusy]);
const handleStopScatterConfirmation = React.useCallback(() => {
setWarnScatterBusy(false);
stopScatteringAll();
handleCurrentFusionStart();
}, [handleCurrentFusionStart, stopScatteringAll]);
const handleStopScatterDenial = React.useCallback(() => setWarnScatterBusy(false), []);
// (this is great ux) scatter freed up while we were asking the question, proceed
React.useEffect(() => {
if (warnScatterBusy && !props.scatterBusy)
handleStopScatterConfirmation();
}, [handleStopScatterConfirmation, props.scatterBusy, warnScatterBusy]);
const handleFactoryActivate = React.useCallback((factoryId: FFactoryId, shiftPressed: boolean) => {
// setStickToBottom(true);
setCurrentFactoryId((factoryId !== currentFactoryId || !shiftPressed) ? factoryId : null);
}, [currentFactoryId, setCurrentFactoryId]);
const MainLlmIcon = gatherLlmIcon || (isGatheringAny ? AutoAwesomeIcon : AutoAwesomeOutlinedIcon);
return <>
return (
<Box
className={`${hasInputs ? gatherPaneClasses.ready : ''} ${isGatheringAny ? gatherPaneClasses.busy : ''}`}
className={`${props.canGather ? gatherPaneClasses.ready : ''} ${isGatheringAny ? gatherPaneClasses.busy : ''}`}
sx={props.isMobile ? mobileGatherPaneSx : desktopGatherPaneSx}
>
@@ -171,7 +118,7 @@ export function BeamGatherPane(props: {
</Typography>
<Typography level='body-sm' sx={{ whiteSpace: 'nowrap' }}>
{/* may merge or not (hasInputs) N replies.. put this in pretty messages */}
{hasInputs ? `Combine the ${gatherCount} replies` : 'Two replies or more'}
{props.canGather ? `Combine the ${props.raysReady} replies` : 'Two replies or more'}
</Typography>
</div>
<ScrollToBottomButton inline />
@@ -179,111 +126,56 @@ export function BeamGatherPane(props: {
{/* Method */}
<FormControl sx={{ my: '-0.25rem' }}>
{!props.isMobile && (
<FormLabelStart
title={<><AutoAwesomeMotionTwoToneIcon sx={{ fontSize: 'md', mr: 0.5 }} />Method</>}
sx={/*{ mb: '0.25rem' }*/ undefined}
/>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ButtonGroup variant='outlined'>
{fusions.map(fusion => {
// get the factory, for additional info
const factory = FUSION_FACTORIES.find(factory => factory.id === fusion.factoryId);
if (!factory) return null;
{/*{!props.isMobile && <FormLabelStart title='Method' />}*/}
<ButtonGroup
variant='outlined'
size='md'
sx={{ boxShadow: isNoFactorySelected ? 'xs' : undefined }}
>
{FUSION_FACTORIES.map(factory => {
const { factoryId, label, isDev } = factory;
// ignore dev fusions, if not asked for it
if (factory.isDev && !gatherShowDevMethods) return null;
// ignore dev fusions, if not asked for it
if (isDev && !gatherShowDevMethods) return null;
// const buttonColor: ColorPaletteProp = fusion.status === 'error' ? 'danger'
// : fusion.status === 'fusing' ? 'warning'
// : fusion.status === 'success' ? GATHER_COLOR
// : fusion.status === 'stopped' ? GATHER_COLOR
// : 'neutral';
const isActive = fusion.fusionId === currentFusionId;
const buttonColor: ColorPaletteProp = isActive /*&& (fusion.status === 'success' || fusion.status === 'stopped')*/
? GATHER_COLOR
: 'neutral';
return (
<Button
key={'fusion-' + fusion.fusionId}
color={buttonColor}
size='sm'
onClick={event => handleFusionActivate(fusion.fusionId, !!event?.shiftKey)}
startDecorator={(isActive && factory.Icon) ? <factory.Icon /> : null}
sx={{
backgroundColor: isActive ? `${buttonColor}.softBg` : 'background.popup',
fontWeight: isActive ? 'lg' : 400, /* reset, from 600 */
// minHeight: '2.25rem',
}}
>
<GoodTooltip title={factory.description}>
<span>
{factory.label}
</span>
</GoodTooltip>
</Button>
);
})}
</ButtonGroup>
{/*{(props.fusionIndex !== null) && (*/}
{/* <Tooltip disableInteractive title='Customize This Merge'>*/}
{/* <IconButton size='sm' color='success' disabled={props.gatherBusy || isEditable..} onClick={handleFusionCopyAsCustom}>*/}
{/* {isEditable... ? null : <EditRoundedIcon />}*/}
{/* </IconButton>*/}
{/* </Tooltip>*/}
{/*)}*/}
</Box>
const isActive = factoryId === currentFactoryId;
const buttonColor: ColorPaletteProp = isActive ? GATHER_COLOR : 'neutral';
return (
<Button
key={'factory-' + factoryId}
color={buttonColor}
onClick={event => handleFactoryActivate(factoryId, !!event?.shiftKey)}
// startDecorator={(isActive && Icon) ? <Icon /> : null}
sx={{
backgroundColor: isActive ? `${buttonColor}.softBg` : 'background.popup',
// fontWeight: isActive ? 'lg' : 'md', /* reset, from 600 */
// minHeight: '2.25rem',
}}
>
{label}
</Button>
);
})}
</ButtonGroup>
</FormControl>
{/* LLM */}
<Box sx={{ my: '-0.25rem', minWidth: 190, maxWidth: 220 }}>
<Box sx={{ my: '-0.25rem', minWidth: 190, maxWidth: 300 }}>
{gatherLlmComponent}
</Box>
{/* Start / Stop buttons */}
{!isGatheringAny ? (
<Button
// key='gather-start' // used for animation triggering, which we don't have now
variant={isCurrentFusionGoodToGo ? 'soft' : 'solid'} color={GATHER_COLOR}
disabled={!gatherEnabled || isGatheringAny} loading={isGatheringAny}
endDecorator={/*CurrentFusionIcon ? <CurrentFusionIcon /> :*/ <MergeRoundedIcon />}
onClick={handleCurrentFusionStart}
sx={BEAM_BTN_SX}
>
Merge
</Button>
) : (
<Button
// key='gather-stop'
variant='solid' color='danger'
endDecorator={<StopRoundedIcon />}
onClick={currentFusionStop}
sx={BEAM_BTN_SX}
>
Stop
</Button>
)}
{/* Add Fusion */}
{/*<FusionAddButton*/}
{/* textOverride='Add'*/}
{/* canGather={props.canGather}*/}
{/* currentFactory={currentFactory}*/}
{/* onAddFusion={props.onAddFusion}*/}
{/* sx={BEAM_BTN_SX}*/}
{/*/>*/}
{/* pad */}
<Box />
</Box>
{/* Confirm Stop Scattering */}
{warnScatterBusy && (
<ConfirmationModal
open
onClose={handleStopScatterDenial}
onPositive={handleStopScatterConfirmation}
// lowStakes
noTitleBar
confirmationText='Some responses are still being generated. Do you want to stop and proceed with merging the available responses now?'
positiveActionText='Proceed with Merge'
negativeActionText='Wait for All Responses'
negativeActionStartDecorator={
<CircularProgress color='neutral' sx={{ '--CircularProgress-size': '24px', '--CircularProgress-trackThickness': '1px' }} />
}
/>
)}
</>;
);
}
+81 -44
View File
@@ -5,8 +5,11 @@ import type { StateCreator } from 'zustand/vanilla';
import type { DLLMId } from '~/modules/llms/store-llms';
import type { DMessage } from '~/common/state/store-chats';
import { FUSION_FACTORIES } from './instructions/beam.gather.factories';
import { GATHER_DEFAULT_TO_FIRST_FUSION, GATHER_PLACEHOLDER } from '../beam.config';
import { FFactoryId, findFusionFactory, FUSION_FACTORIES } from './instructions/beam.gather.factories';
import { GATHER_PLACEHOLDER } from '../beam.config';
import { RootStoreSlice } from '../store-beam-vanilla';
import { ScatterStoreSlice } from '../scatter/beam.scatter';
import { gatherStartFusion, gatherStopFusion, Instruction } from './instructions/beam.gather.execution';
@@ -25,8 +28,11 @@ type BFusionStage =
export interface BFusion {
// const
readonly fusionId: BFusionId;
readonly factoryId: string;
readonly instructions: Readonly<Instruction[]>;
readonly factoryId: FFactoryId;
// options
instructions: Instruction[];
llmId: DLLMId | null;
// status
stage: BFusionStage;
@@ -39,11 +45,14 @@ export interface BFusion {
fusingInstructionComponent?: React.ReactNode;
}
const createBFusion = (factoryId: string, instructions: Instruction[]): BFusion => ({
const createBFusion = (factoryId: FFactoryId, instructions: Instruction[], llmId: DLLMId | null): BFusion => ({
// const
fusionId: uuidv4(),
factoryId,
// options
instructions,
llmId,
// status
stage: 'idle',
@@ -69,6 +78,10 @@ export function fusionIsFusing(fusion: BFusion | null): boolean {
return fusion?.stage === 'fusing';
}
export function fusionIsStopped(fusion: BFusion | null): boolean {
return fusion?.stage === 'stopped';
}
export function fusionIsUsableOutput(fusion: BFusion | null): boolean {
const message = fusion?.outputDMessage ?? null;
return !!message && !!message.updated && !!message.text && message.text !== GATHER_PLACEHOLDER;
@@ -83,9 +96,9 @@ export function fusionIsError(fusion: BFusion | null): boolean {
interface GatherStateSlice {
lastGatherLlmId: DLLMId | null;
currentFactoryId: FFactoryId | null;
currentGatherLlmId: DLLMId | null;
currentFusionId: BFusionId | null;
fusions: BFusion[];
// derived state (just acts as a cache to avoid re-calculating)
@@ -97,14 +110,11 @@ export const reInitGatherStateSlice = (prevFusions: BFusion[], gatherLlmId: DLLM
// stop any ongoing fusions
prevFusions.forEach(gatherStopFusion);
// fully use new fusions
const newFusions = FUSION_FACTORIES.map(factory => createBFusion(factory.id, factory.createInstructions()));
return {
lastGatherLlmId: gatherLlmId, // may be re-set during open() of the Beam Store
currentFactoryId: null,
currentGatherLlmId: gatherLlmId, // may be re-set during open() of the Beam Store
currentFusionId: (GATHER_DEFAULT_TO_FIRST_FUSION && newFusions.length) ? newFusions[0].fusionId : null,
fusions: newFusions,
fusions: [],
isGatheringAny: false,
};
@@ -114,42 +124,36 @@ export type FusionUpdateOrFn = Partial<BFusion> | ((fusion: BFusion) => (Partial
export interface GatherStoreSlice extends GatherStateSlice {
setLastGatherLlmId: (llmId: DLLMId | null) => void;
setCurrentFusionId: (fusionId: BFusionId | null) => void;
_currentFusion: () => BFusion | null;
setCurrentGatherLlmId: (llmId: DLLMId | null) => void;
setCurrentFactoryId: (factoryId: FFactoryId | null) => void;
_fusionUpdate: (fusionId: BFusionId, update: FusionUpdateOrFn) => void;
fusionRecreateAsCustom: (sourceFusionId: BFusionId) => void;
fusionInstructionUpdate: (fusionId: BFusionId, instructionIndex: number, update: Partial<Instruction>) => void;
fusionSetLlmId: (fusionId: BFusionId, llmId: DLLMId | null) => void;
currentFusionStart: (chatHistory: DMessage[], rays: DMessage[]) => void;
currentFusionStop: () => void;
createFusion: () => void;
removeFusion: (fusionId: BFusionId) => void;
toggleFusionGathering: (fusionId: BFusionId) => void;
}
export const createGatherSlice: StateCreator<GatherStoreSlice, [], [], GatherStoreSlice> = (_set, _get) => ({
export const createGatherSlice: StateCreator<RootStoreSlice & ScatterStoreSlice & GatherStoreSlice, [], [], GatherStoreSlice> = (_set, _get) => ({
// initial state
...reInitGatherStateSlice([], null),
setLastGatherLlmId: (llmId: DLLMId | null) =>
setCurrentFactoryId: (factoryId: FFactoryId | null) =>
_set({
lastGatherLlmId: llmId,
currentFactoryId: factoryId,
}),
setCurrentFusionId: (fusionId: BFusionId | null) =>
setCurrentGatherLlmId: (llmId: DLLMId | null) =>
_set({
currentFusionId: fusionId,
currentGatherLlmId: llmId,
}),
_currentFusion: () => {
const { currentFusionId, fusions } = _get();
return currentFusionId !== null ? fusions.find(fusion => fusion.fusionId === currentFusionId) ?? null : null;
},
_fusionUpdate: (fusionId: BFusionId, update: FusionUpdateOrFn) => {
const { fusions } = _get();
@@ -169,16 +173,16 @@ export const createGatherSlice: StateCreator<GatherStoreSlice, [], [], GatherSto
},
fusionRecreateAsCustom: (sourceFusionId: BFusionId) => {
const { fusions } = _get();
const { fusions, currentGatherLlmId } = _get();
// finds the fusion and its factory
const sourceFusion = fusions.find(fusion => fusion.fusionId === sourceFusionId);
const sourceFusionFactory = sourceFusion ? FUSION_FACTORIES.find(spec => spec.id === sourceFusion.factoryId) : undefined;
const sourceFusionFactory = findFusionFactory(sourceFusion?.factoryId);
if (!sourceFusion || !sourceFusionFactory)
return;
// create a custom from the source fusion factory
const newCustomFusion: BFusion = createBFusion('custom', sourceFusionFactory.createInstructions());
const newCustomFusion: BFusion = createBFusion('custom', sourceFusionFactory.createInstructions(), currentGatherLlmId);
// replace the only editable fusion with the new custom fusion
_set({
@@ -187,7 +191,6 @@ export const createGatherSlice: StateCreator<GatherStoreSlice, [], [], GatherSto
gatherStopFusion(_f);
return newCustomFusion;
}),
currentFusionId: newCustomFusion.fusionId,
});
},
@@ -199,21 +202,55 @@ export const createGatherSlice: StateCreator<GatherStoreSlice, [], [], GatherSto
),
})),
fusionSetLlmId: (fusionId: BFusionId, llmId: DLLMId | null) =>
_get()._fusionUpdate(fusionId, {
llmId,
}),
currentFusionStart: (chatHistory: DMessage[], rays: DMessage[]) => {
const { lastGatherLlmId, _currentFusion, _fusionUpdate } = _get();
const fusion = _currentFusion();
createFusion: () => {
// get factory
const { currentFactoryId, currentGatherLlmId, fusions, toggleFusionGathering } = _get();
const factory = FUSION_FACTORIES.find(factory => factory.factoryId === currentFactoryId);
if (!factory)
return;
// create and append the fusion
const newFusion = createBFusion(factory.factoryId, factory.createInstructions(), currentGatherLlmId);
_set({
fusions: [...fusions, newFusion],
});
// start the fusion
toggleFusionGathering(newFusion.fusionId);
},
removeFusion: (fusionId: BFusionId) => {
const fusion = _get().fusions.find(fusion => fusion.fusionId === fusionId);
if (fusion) {
const onUpdate = (update: FusionUpdateOrFn) => _fusionUpdate(fusion.fusionId, update);
gatherStartFusion(fusion, chatHistory, rays, lastGatherLlmId, onUpdate);
gatherStopFusion(fusion);
_set(state => ({
fusions: state.fusions.filter(fusion => fusion.fusionId !== fusionId),
}));
}
},
currentFusionStop: () => {
const { _currentFusion, _fusionUpdate } = _get();
const fusion = _currentFusion();
if (fusion)
_fusionUpdate(fusion.fusionId, gatherStopFusion(fusion));
toggleFusionGathering: (fusionId: BFusionId) => {
// this will start/stop the fusion
const fusion = _get().fusions.find(fusion => fusion.fusionId === fusionId);
if (!fusion) return;
// stop if fusing
if (fusion?.stage === 'fusing')
return gatherStopFusion(fusion);
// start if idle/stopped
const { inputHistory, rays, currentGatherLlmId, _fusionUpdate } = _get();
const chatMessages = inputHistory ? [...inputHistory] : [];
const rayMessages = rays.map(ray => ray.message);
const onUpdate = (update: FusionUpdateOrFn) => _fusionUpdate(fusion.fusionId, update);
gatherStartFusion(fusion, chatMessages, rayMessages, currentGatherLlmId, onUpdate);
},
});
@@ -13,7 +13,7 @@ import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
import type { BaseInstruction, ExecutionInputState } from './beam.gather.execution';
import { GATHER_PLACEHOLDER } from '../../beam.config';
import { fusionChatMessageSx } from '../BeamGatherOutput';
import { beamCardMessageSx } from '../../BeamCard';
type ChatGenerateMethods =
@@ -83,7 +83,7 @@ export async function executeChatGenerate(_i: ChatGenerateInstruction, inputs: E
fitScreen={true}
showAvatar={false}
adjustContentScaling={-1}
sx={fusionChatMessageSx}
sx={beamCardMessageSx}
/>,
);
return;
@@ -64,7 +64,7 @@ export function UserInputChecklistComponent(props: {
return (
<Box sx={{ display: 'grid', gap: 2 }}>
<Typography color='primary' sx={{ mt: 1, fontWeight: 'lg', fontSize: 'md' }}>
<Typography sx={{ mt: 1, fontWeight: 'md', fontSize: 'sm' }}>
Select the Merge options to apply:
</Typography>
@@ -130,7 +130,7 @@ export function gatherStartFusion(
if (inputState.chainAbortController.signal.aborted) {
return onUpdateBFusion({
stage: 'stopped',
errorText: 'Merge Canceled.',
// errorText: 'Merge Canceled.',
fusingProgressComponent: undefined,
});
}
@@ -1,5 +1,4 @@
import * as React from 'react';
import type { SvgIcon } from '@mui/material';
import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined';
import MediationOutlinedIcon from '@mui/icons-material/MediationOutlined';
import TableViewRoundedIcon from '@mui/icons-material/TableViewRounded';
@@ -7,23 +6,31 @@ import TableViewRoundedIcon from '@mui/icons-material/TableViewRounded';
import type { Instruction } from './beam.gather.execution';
interface FusionFactorySpec {
id: 'guided' | 'fuse' | 'eval' | 'custom';
export type FFactoryId = string;
export interface FusionFactorySpec {
factoryId: FFactoryId;
label: string;
Icon?: React.FunctionComponent;
addLabel: string;
Icon?: typeof SvgIcon;
description: string;
isDev?: boolean;
createInstructions: () => Instruction[];
}
export function findFusionFactory(factoryId?: FFactoryId | null): FusionFactorySpec | null {
if (!factoryId) return null;
return FUSION_FACTORIES.find(f => f.factoryId === factoryId) ?? null;
}
export const FUSION_FACTORIES: FusionFactorySpec[] = [
// 1: Guided (Checklist - 3x steps)
{
id: 'guided',
factoryId: 'guided',
label: 'Guided',
addLabel: 'Add Checklist',
Icon: CheckBoxOutlinedIcon,
description: 'A brainstorming session with AI, where you first pick your favorite ideas from a list it generates, and then the AI combines those picks into a tailored solution.',
description: 'Choose between options extracted by AI from the replies, and the model will combine your selections into a single answer.',
// description: 'This approach employs a two-stage, interactive process where an AI first generates a checklist of insights from a conversation for user selection, then synthesizes those selections into a tailored, comprehensive response, integrating user preferences with AI analysis and creativity.',
createInstructions: () => [
{
@@ -82,10 +89,11 @@ The final output should reflect a deep understanding of the user's preferences a
// 2: Fuse
{
id: 'fuse',
factoryId: 'fuse',
label: 'Fuse',
addLabel: 'Add Fusion',
Icon: MediationOutlinedIcon,
description: 'AI combines conversation details and various AI-generated ideas into one clear, comprehensive answer, making sense of diverse insights for you.',
description: 'AI combines conversation details and ideas into one clear, comprehensive answer.',
createInstructions: () => [
{
type: 'chat-generate',
@@ -108,10 +116,11 @@ Synthesize the perfect cohesive response to my last message that merges the coll
// 3: Eval
{
id: 'eval',
factoryId: 'eval',
label: 'Eval',
addLabel: 'Add Critique',
Icon: TableViewRoundedIcon,
description: 'Analyzes and ranks AI responses, offering a clear, comparative overview to support your choice of answer.',
description: 'Analyzes and compares AI responses, offering a structured framework to support your response choice.',
isDev: true,
createInstructions: () => [
{
@@ -152,8 +161,9 @@ Only work with the provided {{N}} responses. Begin with listing the criteria.`.t
// 4: Custom (this may be overwritten by other factories, if editing those)
{
id: 'custom',
factoryId: 'custom',
label: 'Custom',
addLabel: 'Add Custom',
// Icon: BuildCircleOutlinedIcon,
description: 'Define your own fusion prompt.',
createInstructions: () => [
+20 -39
View File
@@ -1,6 +1,4 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, SvgIconProps } from '@mui/joy';
import CheckCircleOutlineRoundedIcon from '@mui/icons-material/CheckCircleOutlineRounded';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
@@ -20,29 +18,13 @@ import { InlineError } from '~/common/components/InlineError';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
import { BeamCard, beamCardClasses } from '../BeamCard';
import { BeamCard, beamCardClasses, beamCardMessageScrollingSx, beamCardMessageSx, beamCardMessageWrapperSx } from '../BeamCard';
import { BeamStoreApi, useBeamStore } from '../store-beam.hooks';
import { GATHER_COLOR, SCATTER_RAY_SHOW_DRAG_HANDLE } from '../beam.config';
import { rayIsError, rayIsImported, rayIsScattering, rayIsSelectable, rayIsUserSelected } from './beam.scatter';
import { useBeamRayScrolling } from '../store-module-beam';
import { useBeamCardScrolling } from '../store-module-beam';
const chatMessageEmbeddedSx: SxProps = {
// style: to undo the style of ChatMessage
backgroundColor: 'none',
border: 'none',
mx: -1.5, // compensates for the marging (e.g. RenderChatText, )
my: 0,
px: 0,
py: 0,
};
const chatMessageEmbeddedScrollingSx: SxProps = {
...chatMessageEmbeddedSx,
overflow: 'auto',
maxHeight: 'max(18rem, calc(60lvh - 16rem))',
};
/*const letterSx: SxProps = {
width: '1rem',
py: 0.25,
@@ -132,7 +114,7 @@ export function BeamRay(props: {
// external state
const ray = useBeamStore(props.beamStore, store => store.rays.find(ray => ray.rayId === props.rayId) ?? null);
const rayScrolling = useBeamRayScrolling();
const cardScrolling = useBeamCardScrolling();
// derived state
const isError = rayIsError(ray);
@@ -140,7 +122,7 @@ export function BeamRay(props: {
const isSelectable = rayIsSelectable(ray);
const isSelected = rayIsUserSelected(ray);
const isImported = rayIsImported(ray);
const showUseButton = isSelectable && !isScattering;
const showUseButtons = isSelectable && !isScattering;
const { removeRay, rayToggleScattering, raySetLlmId } = props.beamStore.getState();
// This old code used the Gather LLM as Ray fallback - but now we use the last Scatter LLM as fallback
@@ -210,27 +192,21 @@ export function BeamRay(props: {
{/* Ray Message */}
{(!!ray?.message?.text || ray?.status === 'scattering') && (
<Box sx={{
minHeight: '1.5rem',
display: 'flex',
flexDirection: 'column',
// uncomment the following to limit the message height
// overflow: 'auto',
// maxHeight: 'calc(0.8 * (100vh - 16rem))',
// aspectRatio: 1,
}}>
<ChatMessageMemo
message={ray.message}
fitScreen={true}
showAvatar={false}
adjustContentScaling={-1}
sx={rayScrolling ? chatMessageEmbeddedScrollingSx : chatMessageEmbeddedSx}
/>
<Box sx={beamCardMessageWrapperSx}>
{!!ray.message && (
<ChatMessageMemo
message={ray.message}
fitScreen={true}
showAvatar={false}
adjustContentScaling={-1}
sx={!cardScrolling ? beamCardMessageSx : beamCardMessageScrollingSx}
/>
)}
</Box>
)}
{/* Use Ray */}
{showUseButton && (
{showUseButtons && (
<Box sx={{ mt: 'auto', mb: -1, mr: -1, placeSelf: 'end', height: 'calc(2.25rem - var(--Pad_2))', position: 'relative' }}>
<Box sx={{
position: 'absolute',
@@ -239,6 +215,8 @@ export function BeamRay(props: {
display: 'flex',
gap: 1,
}}>
{/* Copy */}
{!isImported && (
<GoodTooltip title='Copy'>
<IconButton
@@ -249,6 +227,8 @@ export function BeamRay(props: {
</IconButton>
</GoodTooltip>
)}
{/* Continue */}
<GoodTooltip title='Choose this message'>
<IconButton
size='sm'
@@ -264,6 +244,7 @@ export function BeamRay(props: {
{isImported ? 'From Chat' : /*'Use'*/ <TelegramIcon />}
</IconButton>
</GoodTooltip>
</Box>
</Box>
)}
+15
View File
@@ -71,6 +71,21 @@ export function BeamRayGrid(props: {
{/* Merges*/}
{/*</Divider>*/}
{/* Fusions */}
{/*{props.fusionIds.map((fusionId) => (*/}
{/* <BeamFusion*/}
{/* key={'fusion-' + fusionId}*/}
{/* beamStore={props.beamStore}*/}
{/* fusionId={fusionId}*/}
{/* />*/}
{/*))}*/}
{/* Add Fusion */}
{/*<BeamFusionAdd*/}
{/* beamStore={props.beamStore}*/}
{/* isMobile={props.isMobile}*/}
{/*/>*/}
</Box>
);
}
@@ -64,7 +64,7 @@ export function BeamScatterDropdown(props: {
const [namingOpened, setNamingOpened] = React.useState(false);
// external state
const { scatterPresets, rayScrolling, addScatterPreset, deleteScatterPreset, toggleRayScrolling } = useModuleBeamStore();
const { scatterPresets, cardScrolling, addScatterPreset, deleteScatterPreset, toggleCardScrolling } = useModuleBeamStore();
// handlers - load/save presets
@@ -137,8 +137,8 @@ export function BeamScatterDropdown(props: {
{/* <Typography level='body-sm'>Beam Options</Typography>*/}
{/*</ListItem>*/}
<MenuItem onClick={toggleRayScrolling}>
<ListItemDecorator>{rayScrolling && <CheckRoundedIcon />}</ListItemDecorator>
<MenuItem onClick={toggleCardScrolling}>
<ListItemDecorator>{cardScrolling && <CheckRoundedIcon />}</ListItemDecorator>
Scroll Responses
</MenuItem>
+2 -2
View File
@@ -90,7 +90,7 @@ const createRootSlice: StateCreator<BeamStore, [], [], RootStoreSlice> = (_set,
// update the model only if the dialog was not already open
...((!wasOpen && initialChatLLMId) && {
lastGatherLlmId: initialChatLLMId,
currentGatherLlmId: initialChatLLMId,
} satisfies Partial<GatherStoreSlice>),
});
},
@@ -98,7 +98,7 @@ const createRootSlice: StateCreator<BeamStore, [], [], RootStoreSlice> = (_set,
terminate: () =>
_set(state => ({
...initRootStateSlice(),
...reInitGatherStateSlice(state.fusions, state.lastGatherLlmId), // remember after termination
...reInitGatherStateSlice(state.fusions, state.currentGatherLlmId), // remember after termination
...reInitScatterStateSlice(state.rays),
})),
+6 -6
View File
@@ -17,7 +17,7 @@ interface BeamScatterPreset {
interface ModuleBeamStore {
// state
scatterPresets: BeamScatterPreset[];
rayScrolling: boolean;
cardScrolling: boolean;
gatherShowDevMethods: boolean;
gatherShowPrompts: boolean;
@@ -26,7 +26,7 @@ interface ModuleBeamStore {
deleteScatterPreset: (id: string) => void;
renameScatterPreset: (id: string, name: string) => void;
toggleRayScrolling: () => void;
toggleCardScrolling: () => void;
toggleGatherShowDevMethods: () => void;
toggleGatherShowPrompts: () => void;
@@ -37,7 +37,7 @@ export const useModuleBeamStore = create<ModuleBeamStore>()(persist(
(_set, _get) => ({
scatterPresets: [],
rayScrolling: false,
cardScrolling: false,
gatherShowDevMethods: true,
gatherShowPrompts: false,
@@ -55,7 +55,7 @@ export const useModuleBeamStore = create<ModuleBeamStore>()(persist(
})),
toggleRayScrolling: () => _set(state => ({ rayScrolling: !state.rayScrolling })),
toggleCardScrolling: () => _set(state => ({ cardScrolling: !state.cardScrolling })),
toggleGatherShowDevMethods: () => _set(state => ({ gatherShowDevMethods: !state.gatherShowDevMethods })),
@@ -68,6 +68,6 @@ export const useModuleBeamStore = create<ModuleBeamStore>()(persist(
));
export function useBeamRayScrolling() {
return useModuleBeamStore((state) => state.rayScrolling);
export function useBeamCardScrolling() {
return useModuleBeamStore((state) => state.cardScrolling);
}