diff --git a/src/apps/chat/components/layout-drawer/ChatDrawerItem.tsx b/src/apps/chat/components/layout-drawer/ChatDrawerItem.tsx index d46a7a011..611133191 100644 --- a/src/apps/chat/components/layout-drawer/ChatDrawerItem.tsx +++ b/src/apps/chat/components/layout-drawer/ChatDrawerItem.tsx @@ -20,6 +20,7 @@ import { autoConversationTitle } from '~/modules/aifn/autotitle/autoTitle'; import type { DConversationId } from '~/common/stores/chat/chat.conversation'; import type { DFolder } from '~/common/stores/folders/store-chat-folders'; import { ANIM_BUSY_TYPING } from '~/common/util/dMessageUtils'; +import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon'; import { InlineTextarea } from '~/common/components/InlineTextarea'; import { isDeepEqual } from '~/common/util/hooks/useDeep'; import { useChatStore } from '~/common/stores/chat/store-chats'; @@ -64,6 +65,7 @@ export interface ChatNavigationItemData { containsImageAssets: boolean; folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select updatedAt: number; + hasBeamOpen: boolean; messageCount: number; beingGenerated: boolean; systemPurposeId: SystemPurposeId; @@ -106,6 +108,7 @@ function ChatDrawerItem(props: { containsDocAttachments, containsImageAssets, folder, + hasBeamOpen, messageCount, beingGenerated, systemPurposeId, @@ -210,7 +213,9 @@ function ChatDrawerItem(props: { {/* Symbol, if globally enabled */} {(props.showSymbols || isIncognito) && ( - {isIncognito ? ( + {hasBeamOpen ? ( + + ) : isIncognito ? ( ) : (beingGenerated && props.showSymbols === 'gif') ? ( ) : null} - , [beingGenerated, containsDocAttachments, containsImageAssets, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isIncognito, isNew, personaImageURI, personaSymbol, props.showSymbols, searchFrequency, title, userFlagsSummary]); + , [beingGenerated, containsDocAttachments, containsImageAssets, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, hasBeamOpen, isActive, isEditingTitle, isIncognito, isNew, personaImageURI, personaSymbol, props.showSymbols, searchFrequency, title, userFlagsSummary]); const progressBarFixedComponent = React.useMemo(() => progress > 0 && ( diff --git a/src/apps/chat/components/layout-drawer/useChatDrawerRenderItems.tsx b/src/apps/chat/components/layout-drawer/useChatDrawerRenderItems.tsx index ed99dfc3e..102bbdce8 100644 --- a/src/apps/chat/components/layout-drawer/useChatDrawerRenderItems.tsx +++ b/src/apps/chat/components/layout-drawer/useChatDrawerRenderItems.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import { useModuleBeamStore } from '~/modules/beam/store-module-beam'; + import type { DFolder } from '~/common/stores/folders/store-chat-folders'; import { DMessage, DMessageUserFlag, MESSAGE_FLAG_STARRED, messageFragmentsReduceText, messageHasUserFlag, messageUserFlagToEmoji } from '~/common/stores/chat/chat.message'; import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation'; @@ -94,6 +96,9 @@ export function useChatDrawerRenderItems( // state const [_, setJustAMinuteCounter] = React.useState(0); + // external state + const openBeamConversationIds = useModuleBeamStore(state => state.openBeamConversationIds); + // [effect] Refresh every minute because the `getTimeBucketEn` function uses the current time React.useEffect(() => { @@ -173,6 +178,7 @@ export function useChatDrawerRenderItems( ? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null : null, updatedAt: _c.updated || _c.created || 0, + hasBeamOpen: !!openBeamConversationIds?.[_c.id], messageCount, beingGenerated: !!_c._abortController, // FIXME: when the AbortController is moved at the message level, derive the state in the conv systemPurposeId: _c.systemPurposeId, diff --git a/src/common/chat-overlay/ConversationHandler.ts b/src/common/chat-overlay/ConversationHandler.ts index c7609cbd7..e296dd3eb 100644 --- a/src/common/chat-overlay/ConversationHandler.ts +++ b/src/common/chat-overlay/ConversationHandler.ts @@ -5,6 +5,7 @@ import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix'; import { SystemPurposes } from '../../data'; import { BeamStore, createBeamVanillaStore } from '~/modules/beam/store-beam_vanilla'; +import { useModuleBeamStore } from '~/modules/beam/store-module-beam'; import type { DConversationId } from '~/common/stores/chat/chat.conversation'; import type { DLLMId } from '~/common/stores/llms/llms.types'; @@ -38,6 +39,12 @@ export class ConversationHandler { constructor(private readonly conversationId: DConversationId) { this.beamStore = createBeamVanillaStore(); this.overlayStore = createPerChatVanillaStore(); + + // track the open status of beams - this is meant to be an accelerator for the UI + this.beamStore.subscribe((state, prevState) => { + if (state.isOpen === prevState.isOpen) return; + useModuleBeamStore.getState().setBeamOpenForConversation(this.conversationId, state.isOpen); + }); } diff --git a/src/modules/beam/store-module-beam.tsx b/src/modules/beam/store-module-beam.tsx index 433f3f3d0..bdf308d71 100644 --- a/src/modules/beam/store-module-beam.tsx +++ b/src/modules/beam/store-module-beam.tsx @@ -1,13 +1,14 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; +import type { DConversationId } from '~/common/stores/chat/chat.conversation'; import type { DLLMId } from '~/common/stores/llms/llms.types'; import { agiUuid } from '~/common/util/idUtils'; import type { FFactoryId } from './gather/instructions/beam.gather.factories'; -/// Presets (persistes as zustand store) /// +/// Presets (persisted as zustand store) /// export interface BeamConfigSnapshot { id: string; @@ -19,6 +20,8 @@ export interface BeamConfigSnapshot { interface ModuleBeamState { + + // stored presets: BeamConfigSnapshot[]; lastConfig: BeamConfigSnapshot | null; cardAdd: boolean; @@ -27,6 +30,10 @@ interface ModuleBeamState { scatterShowPrevMessages: boolean; gatherAutoStartAfterScatter: boolean; gatherShowAllPrompts: boolean; + + // non-stored, temporary but useful for the UI + openBeamConversationIds: Record; + } interface ModuleBeamStore extends ModuleBeamState { @@ -43,6 +50,9 @@ interface ModuleBeamStore extends ModuleBeamState { toggleScatterShowPrevMessages: () => void; toggleGatherAutoStartAfterScatter: () => void; toggleGatherShowAllPrompts: () => void; + + setBeamOpenForConversation: (conversationId: DConversationId, isOpen: boolean) => void; + clearBeamOpenForConversation: (conversationId: DConversationId) => void; } @@ -57,6 +67,7 @@ export const useModuleBeamStore = create()(persist( scatterShowPrevMessages: false, gatherShowAllPrompts: false, gatherAutoStartAfterScatter: false, + openBeamConversationIds: {}, addPreset: (name, rayLlmIds, gatherLlmId, gatherFactoryId) => _set(state => ({ @@ -99,11 +110,32 @@ export const useModuleBeamStore = create()(persist( toggleGatherShowAllPrompts: () => _set(state => ({ gatherShowAllPrompts: !state.gatherShowAllPrompts })), + setBeamOpenForConversation: (conversationId, isOpen) => _set(state => { + const openBeams = { ...state.openBeamConversationIds }; + if (isOpen) + openBeams[conversationId] = true; + else + delete openBeams[conversationId]; + return { openBeamConversationIds: openBeams }; + }), + + clearBeamOpenForConversation: (conversationId) => _set(state => { + const openBeams = { ...state.openBeamConversationIds }; + delete openBeams[conversationId]; + return { openBeamConversationIds: openBeams }; + }), + }), { name: 'app-module-beam', version: 1, - migrate: (state: any, fromVersion: number): ModuleBeamState => { + partialize: (state) => { + // exclude openBeamConversationIds from persistence + const { openBeamConversationIds, ...persistedState } = state; + return persistedState; + }, + + migrate: (state: any, fromVersion: number): Omit => { // 0 -> 1: rename 'scatterPresets' to 'presets' if (state && fromVersion === 0 && !state.presets) return { ...state, presets: state.scatterPresets || [] }; @@ -125,6 +157,10 @@ export function useBeamScatterShowLettering() { return useModuleBeamStore((state) => state.scatterShowLettering); } +export function useIsBeamOpenForConversation(conversationId: DConversationId | null): boolean { + return useModuleBeamStore(state => conversationId ? state.openBeamConversationIds[conversationId] ?? false : false); +} + export function updateBeamLastConfig(update: Partial) { useModuleBeamStore.getState().updateLastConfig(update); } \ No newline at end of file