mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
AutoPrompts: cleanup
This commit is contained in:
@@ -14,18 +14,20 @@ import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { useChatMicTimeoutMsValue } from '../../store-app-chat';
|
||||
import { useChatAutoSuggestAttachmentPrompts, useChatMicTimeoutMsValue } from '../../store-app-chat';
|
||||
|
||||
import type { DLLM } from '~/modules/llms/store-llms';
|
||||
import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vendor';
|
||||
import { useAgiAttachmentPrompts } from '~/modules/aifn/attachmentprompts/useAgiAttachmentPrompts';
|
||||
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { AudioGenerator } from '~/common/util/audio/AudioGenerator';
|
||||
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
|
||||
import { ButtonAttachFilesMemo } from '~/common/components/ButtonAttachFiles';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
|
||||
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
|
||||
import { DMessageMetadata, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
|
||||
import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { browserSpeechRecognitionCapability, SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
@@ -42,7 +44,6 @@ import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useChatOverlayStore } from '~/common/chat-overlay/store-chat-overlay';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
@@ -142,7 +143,7 @@ export function Composer(props: {
|
||||
|
||||
// composer-overlay: for the reply-to state, comes from the conversation overlay
|
||||
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
|
||||
replyToGenerateText: (chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1') ? store.replyToText?.trim() || null : null,
|
||||
replyToGenerateText: (chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1') ? store.replyToText?.trim() ?? undefined : undefined,
|
||||
})));
|
||||
|
||||
// don't load URLs if the user is typing a command or there's no capability
|
||||
@@ -161,6 +162,11 @@ export function Composer(props: {
|
||||
// drag/drop
|
||||
const { dragContainerSx, dragDropComponent, handleDragEnter, handleDragStart } = useDragDrop(!!props.isMobile, attachAppendDataTransfer);
|
||||
|
||||
// ai functions
|
||||
const {
|
||||
agiAttachmentPrompts, agiAttachmentPromptsComponent, agiAttachmentPromptsRefetch,
|
||||
} = useAgiAttachmentPrompts(useChatAutoSuggestAttachmentPrompts(), attachmentDrafts);
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
@@ -253,7 +259,7 @@ export function Composer(props: {
|
||||
}, [attachmentsTakeAllFragments, handleClear, onAction, replyToGenerateText, targetConversationId]);
|
||||
|
||||
|
||||
const handleAppendAndSend = React.useCallback(async (appendText: string) => {
|
||||
const handleAppendTextAndSend = React.useCallback(async (appendText: string) => {
|
||||
const newText = composeText ? `${composeText} ${appendText}` : appendText;
|
||||
setComposeText(newText);
|
||||
await handleSendAction(chatExecuteMode, newText);
|
||||
@@ -664,11 +670,11 @@ export function Composer(props: {
|
||||
// onBlurCapture={handleFocusModeOff}
|
||||
endDecorator={
|
||||
<ComposerTextAreaActions
|
||||
attachmentDrafts={attachmentDrafts}
|
||||
showChatReplyTo={showChatReplyTo}
|
||||
replyToGenerateText={replyToGenerateText}
|
||||
onAppendAndSend={handleAppendAndSend}
|
||||
agiAttachmentButton={agiAttachmentPromptsComponent}
|
||||
agiAttachmentPrompts={agiAttachmentPrompts}
|
||||
onAppendAndSend={handleAppendTextAndSend}
|
||||
onReplyToClear={handleReplyToClear}
|
||||
replyToText={replyToGenerateText}
|
||||
/>
|
||||
}
|
||||
slotProps={{
|
||||
@@ -759,6 +765,7 @@ export function Composer(props: {
|
||||
llmAttachmentDrafts={llmAttachmentDraftsCollection.llmAttachmentDrafts}
|
||||
canInlineSomeFragments={llmAttachmentDraftsCollection.canInlineSomeFragments}
|
||||
onAttachmentDraftsAction={handleAttachmentDraftsAction}
|
||||
onAgiAttachmentPromptsRefetch={agiAttachmentPromptsRefetch}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,44 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Alert, Box, Button, CircularProgress, Sheet } from '@mui/joy';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
|
||||
import { proposeActionsForAttachments } from '~/modules/aifn/autoprompts/autoprompts';
|
||||
|
||||
import type { AttachmentDraft } from '~/common/attachment-drafts/attachment.types';
|
||||
import { useShallowStable } from '~/common/util/hooks/useShallowObject';
|
||||
import { Box, Sheet } from '@mui/joy';
|
||||
|
||||
import { ReplyToBubble } from '../message/ReplyToBubble';
|
||||
import { getChatAutoAI } from '../../store-app-chat';
|
||||
|
||||
|
||||
export function ComposerTextAreaActions(props: {
|
||||
attachmentDrafts: AttachmentDraft[],
|
||||
showChatReplyTo: boolean,
|
||||
replyToGenerateText: string | null,
|
||||
agiAttachmentButton?: React.ReactNode,
|
||||
agiAttachmentPrompts?: string[],
|
||||
replyToText?: string,
|
||||
onAppendAndSend: (appendText: string) => Promise<void>,
|
||||
onReplyToClear: () => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { autoSuggestAttachmentPrompts } = getChatAutoAI();
|
||||
|
||||
const allFragments = useShallowStable(props.attachmentDrafts.flatMap(draft => draft.outputFragments));
|
||||
|
||||
const enableAttachmentGuess = autoSuggestAttachmentPrompts && allFragments.length >= 2;
|
||||
|
||||
const { data: attachmentInstructionCandidates, error, isPending, isFetching, refetch } = useQuery({
|
||||
enabled: enableAttachmentGuess,
|
||||
queryKey: ['attachment-guess', ...allFragments.map(f => f.fId).sort()],
|
||||
queryFn: async (context) => proposeActionsForAttachments(allFragments, context.signal),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
const handleUpdateAttachmentGuess = React.useCallback(async () => await refetch(), [refetch]);
|
||||
|
||||
|
||||
if (!props.showChatReplyTo && !enableAttachmentGuess)
|
||||
// skip the component if there's nothing to show
|
||||
if (!props.agiAttachmentPrompts?.length && !props.agiAttachmentButton && props.replyToText === undefined)
|
||||
return null;
|
||||
|
||||
return (
|
||||
@@ -63,17 +39,17 @@ export function ComposerTextAreaActions(props: {
|
||||
}}>
|
||||
|
||||
{/* Reply-To bubble */}
|
||||
{props.showChatReplyTo && (
|
||||
{props.replyToText !== undefined && (
|
||||
<ReplyToBubble
|
||||
replyToText={props.replyToGenerateText}
|
||||
replyToText={props.replyToText}
|
||||
onClear={props.onReplyToClear}
|
||||
className='reply-to-bubble'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User Prompt candidates */}
|
||||
{enableAttachmentGuess && !!attachmentInstructionCandidates?.length && (
|
||||
attachmentInstructionCandidates.map((candidate, index) => (
|
||||
{/* Auto-Prompts from attachments */}
|
||||
{!!props.agiAttachmentPrompts?.length && (
|
||||
props.agiAttachmentPrompts.map((candidate, index) => (
|
||||
<Sheet
|
||||
key={index}
|
||||
color='primary'
|
||||
@@ -103,31 +79,7 @@ export function ComposerTextAreaActions(props: {
|
||||
)}
|
||||
|
||||
{/* Guess Action Button */}
|
||||
|
||||
{enableAttachmentGuess && <Box sx={{ display: 'flex', gap: 1, mb: 0.5 }}>
|
||||
|
||||
{/* Guess / Guess Again */}
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
disabled={isFetching}
|
||||
endDecorator={isFetching ? <CircularProgress color='neutral' sx={{ '--CircularProgress-size': '16px' }} /> : <AutoFixHighIcon sx={{ fontSize: '20px' }} />}
|
||||
onClick={handleUpdateAttachmentGuess}
|
||||
sx={{
|
||||
px: 3,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: '0 4px 6px -4px rgb(var(--joy-palette-primary-darkChannel) / 40%)',
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
{isFetching ? 'Guessing what to do...' : isPending ? 'Guess what to do' : 'What else could we do'}
|
||||
</Button>
|
||||
|
||||
{!!error && <Alert variant='soft' color='danger'>
|
||||
{error.message || 'Error guessing actions'}
|
||||
</Alert>}
|
||||
|
||||
</Box>}
|
||||
{props.agiAttachmentButton}
|
||||
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem } from '@mui/joy';
|
||||
import { Box, CircularProgress, IconButton, ListDivider, ListItemDecorator, MenuItem } from '@mui/joy';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
@@ -8,6 +9,7 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { useAsyncCall } from '~/common/util/hooks/useAsyncCall';
|
||||
|
||||
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
|
||||
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts-slice';
|
||||
@@ -28,12 +30,14 @@ export function LLMAttachmentsList(props: {
|
||||
llmAttachmentDrafts: LLMAttachmentDraft[];
|
||||
canInlineSomeFragments: boolean;
|
||||
onAttachmentDraftsAction: (attachmentDraftId: AttachmentDraftId | null, actionId: LLMAttachmentDraftsAction) => void,
|
||||
onAgiAttachmentPromptsRefetch: () => Promise<any>,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [confirmClearAttachmentDrafts, setConfirmClearAttachmentDrafts] = React.useState<boolean>(false);
|
||||
const [draftMenu, setDraftMenu] = React.useState<{ anchor: HTMLAnchorElement, attachmentDraftId: AttachmentDraftId } | null>(null);
|
||||
const [overallMenuAnchor, setOverallMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
|
||||
const [overallIsAgiRefetchingPrompts, handleOverallAgiPromptsRefetch] = useAsyncCall(props.onAgiAttachmentPromptsRefetch);
|
||||
|
||||
// derived state
|
||||
|
||||
@@ -155,6 +159,14 @@ export function LLMAttachmentsList(props: {
|
||||
anchorEl={overallMenuAnchor} onClose={handleOverallMenuHide}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
{/* uses the agiAttachmentPrompts to imagine what the user will ask aboud those */}
|
||||
<MenuItem color='primary' variant='soft' onClick={handleOverallAgiPromptsRefetch} disabled={!hasAttachments || overallIsAgiRefetchingPrompts}>
|
||||
<ListItemDecorator>{overallIsAgiRefetchingPrompts ? <CircularProgress size='sm' /> : <AutoFixHighIcon />}</ListItemDecorator>
|
||||
What to do?
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<MenuItem onClick={handleOverallInlineText} disabled={!canInlineSomeFragments}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
Inline all text
|
||||
@@ -163,7 +175,9 @@ export function LLMAttachmentsList(props: {
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
Copy all text
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<MenuItem onClick={handleOverallClear}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Remove All{llmAttachmentDrafts.length > 5 ? <span style={{ opacity: 0.5 }}> {llmAttachmentDrafts.length} attachments</span> : null}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const inlineMessageBubbleSx: SxProps = {
|
||||
|
||||
|
||||
export function ReplyToBubble(props: {
|
||||
replyToText: string | null,
|
||||
replyToText?: string,
|
||||
inlineUserMessage?: boolean
|
||||
onClear?: () => void,
|
||||
className?: string,
|
||||
|
||||
@@ -156,6 +156,9 @@ export const getChatAutoAI = (): {
|
||||
export const useChatAutoSuggestHTMLUI = (): boolean =>
|
||||
useAppChatStore(state => state.autoSuggestHTMLUI);
|
||||
|
||||
export const useChatAutoSuggestAttachmentPrompts = (): boolean =>
|
||||
useAppChatStore(state => state.autoSuggestAttachmentPrompts);
|
||||
|
||||
export const useChatMicTimeoutMsValue = (): number =>
|
||||
useAppChatStore(state => state.micTimeoutMs);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
interface AsyncState<TData> {
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
data: TData | null;
|
||||
lastSuccessfulData: TData | null;
|
||||
}
|
||||
|
||||
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
|
||||
@@ -22,9 +22,9 @@ type AsyncCallResult<TArgs extends any[], TResult> = [
|
||||
* Ideal for standardizing async patterns in React applications, particularly
|
||||
* for API calls, data fetching, and other asynchronous tasks:
|
||||
*
|
||||
* - react to state: loading, error, and data being set
|
||||
* - react to state: LOADING (the main reason for this), error, and data being set
|
||||
* - type safe: maintain typescript types throughout
|
||||
* - misc: prevent stale closures, memory leaks (on unmount), boilerplate
|
||||
* - misc: prevent stale closures, memory leaks (on unmount - NOTE: REMOVED!), boilerplate
|
||||
*/
|
||||
export function useAsyncCall<TArgs extends any[], TResult>(asyncFunction: AsyncFunction<TArgs, TResult>): AsyncCallResult<TArgs, TResult> {
|
||||
|
||||
@@ -32,40 +32,40 @@ export function useAsyncCall<TArgs extends any[], TResult>(asyncFunction: AsyncF
|
||||
const [state, setState] = React.useState<AsyncState<TResult>>({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
data: null,
|
||||
lastSuccessfulData: null,
|
||||
});
|
||||
|
||||
const isMounted = React.useRef(true);
|
||||
const latestAsyncFunction = React.useRef(asyncFunction);
|
||||
|
||||
React.useEffect(() => {
|
||||
latestAsyncFunction.current = asyncFunction;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [asyncFunction]);
|
||||
|
||||
const execute = React.useCallback(async (...args: TArgs): Promise<TResult> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await latestAsyncFunction.current(...args);
|
||||
if (isMounted.current) {
|
||||
setState({ isLoading: false, error: null, data: result });
|
||||
}
|
||||
setState({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastSuccessfulData: result,
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('An error occurred');
|
||||
if (isMounted.current) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error,
|
||||
}));
|
||||
}
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error,
|
||||
}));
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [state.isLoading, execute, state.error, state.data];
|
||||
return [state.isLoading, execute, state.error, state.lastSuccessfulData];
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ function aixSystemMessage(text: string) {
|
||||
}
|
||||
|
||||
|
||||
export async function proposeActionsForAttachments(allFragments: DMessageAttachmentFragment[], abortSignal: AbortSignal) {
|
||||
export async function agiAttachmentPrompts(allFragments: DMessageAttachmentFragment[], abortSignal: AbortSignal) {
|
||||
// sanity checks
|
||||
const llmId = getChatLLMId();
|
||||
const docParts = allFragments.filter(f => f.part.pt === 'doc').map(f => f.part) as DMessageDocPart[];
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Alert, Box, Button, CircularProgress } from '@mui/joy';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
|
||||
import type { AttachmentDraft } from '~/common/attachment-drafts/attachment.types';
|
||||
import { useShallowStable } from '~/common/util/hooks/useShallowObject';
|
||||
|
||||
import { agiAttachmentPrompts } from './agiAttachmentPrompts';
|
||||
|
||||
|
||||
export function useAgiAttachmentPrompts(automatic: boolean, attachmentDrafts: AttachmentDraft[]) {
|
||||
|
||||
// external state
|
||||
const stableFragments = useShallowStable(attachmentDrafts.flatMap(draft => draft.outputFragments));
|
||||
|
||||
// derived state
|
||||
const automaticTrigger = automatic && stableFragments.length >= 2;
|
||||
|
||||
// async operation state
|
||||
const { data: prompts, error, isPending, isFetching, isLoading, isRefetching, isStale, refetch } = useQuery({
|
||||
enabled: automaticTrigger,
|
||||
queryKey: ['aifn-prompts-attachments', ...stableFragments.map(f => f.fId).sort()],
|
||||
queryFn: async ({ signal }) => agiAttachmentPrompts(stableFragments, signal),
|
||||
staleTime: Infinity,
|
||||
// placeholderData: keepPreviousData,
|
||||
});
|
||||
console.log('state', { isPending, isFetching, isLoading, isRefetching, isStale });
|
||||
// callbacks
|
||||
const handleRefetch = React.useCallback(async () => refetch(), [refetch]);
|
||||
|
||||
// memoed components
|
||||
|
||||
const hasData = !!prompts && prompts.length > 0;
|
||||
const showComponent = hasData || automaticTrigger;
|
||||
|
||||
const component = React.useMemo(() => {
|
||||
return !showComponent ? null : (
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 0.5 }}>
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
disabled={isFetching}
|
||||
endDecorator={
|
||||
isFetching ? <CircularProgress color='neutral' sx={{ '--CircularProgress-size': '16px' }} />
|
||||
: <AutoFixHighIcon sx={{ fontSize: '20px' }} />
|
||||
}
|
||||
onClick={handleRefetch}
|
||||
sx={{
|
||||
px: 3,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: '0 4px 6px -4px rgb(var(--joy-palette-primary-darkChannel) / 40%)',
|
||||
borderRadius: 'sm',
|
||||
}}
|
||||
>
|
||||
{isFetching ? 'Guessing what to do...' : isPending ? 'Guess what to do' : 'What else could we do'}
|
||||
</Button>
|
||||
|
||||
{!!error && (
|
||||
<Alert variant='soft' color='danger'>
|
||||
{error.message || 'Error guessing actions'}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}, [error, handleRefetch, isFetching, isPending, showComponent]);
|
||||
|
||||
return {
|
||||
agiAttachmentPrompts: prompts,
|
||||
agiAttachmentPromptsRefetch: refetch,
|
||||
agiAttachmentPromptsComponent: component,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user