Aix: +Diagram

This commit is contained in:
Enrico Ros
2024-10-10 17:56:45 -07:00
parent ed98829869
commit 054a8d9050
5 changed files with 69 additions and 40 deletions
+38 -22
View File
@@ -10,13 +10,13 @@ import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
import TelegramIcon from '@mui/icons-material/Telegram';
import { AutoBlocksRenderer } from '~/modules/blocks/AutoBlocksRenderer';
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
import { aixChatGenerateText_Simple } from '~/modules/aix/client/aix.client';
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { InlineError } from '~/common/components/InlineError';
import { adjustContentScaling } from '~/common/app.theme';
import { createDMessageTextContent, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useFormRadio } from '~/common/components/forms/useFormRadio';
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
import { useIsMobile } from '~/common/components/useMatchMedia';
@@ -29,10 +29,10 @@ import { bigDiagramPrompt, DiagramLanguage, diagramLanguages, DiagramType, diagr
const DIAGRAM_ACTOR_PREFIX = 'diagram';
// Used by the callers to setup the diagam session
// Used by the callers to setup the diagram session
export interface DiagramConfig {
conversationId: string;
messageId: string,
messageId: string;
text: string;
}
@@ -69,9 +69,11 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
const [diagramLlm, llmComponent] = useFormRadioLlmType('Generator', 'chat');
// derived state
const { conversationId, messageId, text: subject } = props.config;
const { messageId, text: subject } = props.config;
const diagramLlmId = diagramLlm?.id;
// conversation handler (to view history and eventually append the message)
const cHandler = ConversationsManager.getHandler(props.config.conversationId);
/**
* Core Diagram Generation function, with Streaming, custom prompt, etc.
@@ -80,13 +82,8 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
if (abortController)
return;
const conversation = useChatStore.getState().conversations.find(c => c.id === conversationId);
if (!diagramType || !diagramLanguage || !diagramLlm || !conversation)
return setErrorMessage('Invalid diagram Type, Language, Generator, or conversation.');
const systemMessage = conversation?.messages?.length ? conversation.messages[0] : null;
if (systemMessage?.role !== 'system')
return setErrorMessage('No System message in this conversation');
if (!diagramType || !diagramLanguage || !diagramLlm)
return setErrorMessage(`Invalid diagram Type, Language, or Model (${diagramLlm}).`);
setErrorMessage(null);
@@ -95,13 +92,28 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
const stepAbortController = new AbortController();
setAbortController(stepAbortController);
// cHandler.setAbortController(stepAbortController);
const systemMessageText = messageFragmentsReduceText(systemMessage.fragments);
const diagramPrompt = bigDiagramPrompt(diagramType, diagramLanguage, systemMessageText, subject, customInstruction);
const history = cHandler.historyViewHead('diagrams-modal');
const systemMessage = history.length > 0 ? history[0] : null;
if (!systemMessage || systemMessage?.role !== 'system')
return setErrorMessage('No System instruction in this conversation');
try {
await llmStreamingChatGenerate(diagramLlm.id, diagramPrompt, 'ai-diagram', messageId, null, null, stepAbortController.signal,
({ textSoFar }) => textSoFar && setDiagramCode(diagramCode = textSoFar),
const { systemInstruction, messages } = bigDiagramPrompt(
diagramType,
diagramLanguage,
messageFragmentsReduceText(systemMessage.fragments),
subject,
customInstruction,
);
await aixChatGenerateText_Simple(
diagramLlm.id,
systemInstruction,
messages,
'ai-diagram', messageId,
{ abortSignal: stepAbortController.signal },
(text) => !!text && setDiagramCode(diagramCode = text.trim()),
);
} catch (error: any) {
setDiagramCode(null);
@@ -111,18 +123,18 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
setAbortController(null);
}
}, [abortController, conversationId, customInstruction, diagramLanguage, diagramLlm, diagramType, messageId, subject]);
}, [abortController, cHandler, customInstruction, diagramLanguage, diagramLlm, diagramType, messageId, subject]);
// [Effect] Auto-abort on unmount
React.useEffect(() => {
return () => {
if (abortController) {
abortController.abort();
// cHandler.setAbortController(null);
setAbortController(null);
}
};
}, [abortController]);
}, [abortController, cHandler]);
// custom instruction
@@ -149,9 +161,9 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
// diagramMessage.purposeId = conversation.systemPurposeId;
diagramMessage.generator = { mgt: 'named', name: DIAGRAM_ACTOR_PREFIX + (diagramLlmId ? `-${diagramLlmId}` : '') };
useChatStore.getState().appendMessage(conversationId, diagramMessage);
cHandler.messageAppend(diagramMessage);
props.onClose();
}, [conversationId, diagramCode, diagramLlmId, props]);
}, [cHandler, diagramCode, diagramLlmId, props]);
// [effect] Auto-switch language to match diagram type
@@ -258,7 +270,11 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
<Button
variant={abortController ? 'soft' : 'solid'} color='primary'
disabled={!diagramLlm}
onClick={abortController ? () => abortController.abort() : handleGenerateNew}
onClick={abortController ? () => {
abortController.abort();
// cHandler.setAbortController(null);
setAbortController(null);
} : handleGenerateNew}
endDecorator={abortController ? <StopOutlinedIcon /> : diagramCode ? <ReplayIcon /> : <AccountTreeTwoToneIcon />}
sx={{ minWidth: isMobile ? 160 : 220 }}
>
+18 -8
View File
@@ -1,10 +1,12 @@
import type { AixChatGenerate_TextMessages } from '~/modules/aix/client/aix.client.chatGenerateRequest';
import type { FormRadioOption } from '~/common/components/forms/FormRadioControl';
import type { VChatMessageIn } from '~/modules/llms/llm.client';
export type DiagramType = 'auto' | 'mind';
export type DiagramLanguage = 'mermaid' | 'plantuml';
// NOTE: keep these global, or it will trigger re-renders
export const diagramTypes: FormRadioOption<DiagramType>[] = [
{ label: 'Automatic', value: 'auto' },
@@ -64,12 +66,20 @@ function mermaidDiagramPrompt(diagramType: DiagramType): { sys: string, usr: str
const sysSuffixPM = 'The next three messages will outline: 1. your personality, 2. the data you\'ll work with, and 3. a clear restatement of the instructions.';
const usrSuffixCoT = 'Please think step by step, then generate valid diagram code in a markdown block as instructed, and stop your response.';
export function bigDiagramPrompt(diagramType: DiagramType, diagramLanguage: DiagramLanguage, chatSystemPrompt: string, subject: string, customInstruction: string): VChatMessageIn[] {
export function bigDiagramPrompt(
diagramType: DiagramType,
diagramLanguage: DiagramLanguage,
chatSystemPrompt: string,
subject: string,
customInstruction: string,
): { systemInstruction: string, messages: AixChatGenerate_TextMessages } {
const { sys, usr } = diagramLanguage === 'mermaid' ? mermaidDiagramPrompt(diagramType) : plantumlDiagramPrompt(diagramType);
return [
{ role: 'system', content: sys + '\n' + sysSuffixPM },
{ role: 'user', content: chatSystemPrompt },
{ role: 'assistant', content: subject },
{ role: 'user', content: (!customInstruction?.trim() ? usr : `${usr} Also consider the following instructions: ${customInstruction.trim()}`) + '\n' + usrSuffixCoT },
];
return {
systemInstruction: sys + '\n' + sysSuffixPM,
messages: [
{ role: 'user', text: chatSystemPrompt },
{ role: 'model', text: subject },
{ role: 'user', text: (!customInstruction?.trim() ? usr : `${usr} Also consider the following instructions: ${customInstruction.trim()}`) + '\n' + usrSuffixCoT },
],
};
}
+7 -7
View File
@@ -75,8 +75,13 @@ export function AutoBlocksRenderer(props: {
// state
const { text, isTextCollapsed, forceTextExpanded, handleToggleExpansion } =
useTextCollapser(props.text, fromUser);
let autoBlocksStable =
useAutoBlocksMemoSemiStable(text, props.renderAsCodeWithTitle, fromSystem, props.renderSanityTextDiffs);
const autoBlocksStable = useAutoBlocksMemoSemiStable(
text,
props.renderAsCodeWithTitle,
fromSystem,
props.renderSanityTextDiffs,
props.blocksProcessor === 'diagram',
);
// handlers
const { setText } = props;
@@ -92,11 +97,6 @@ export function AutoBlocksRenderer(props: {
}, [setText, text]);
// apply specialDiagramMode filter if applicable
if (props.blocksProcessor === 'diagram')
autoBlocksStable = autoBlocksStable.filter(({ bkt }) => bkt === 'code-bk' || autoBlocksStable.length === 1);
// Memo the styles, to minimize re-renders
const scaledCodeSx = useScaledCodeSx(fromAssistant, props.contentScaling, props.codeRenderVariant || 'outlined');
const scaledImageSx = useScaledImageSx(props.contentScaling);
+5 -2
View File
@@ -61,7 +61,7 @@ function areBlocksEqualIdIgnored(block1: RenderBlockInputs[number] | undefined,
* as part of the the running text, in which case the growing text will be
* reassigned (when it's chopped to before the code block, in the next call)
*/
export function useAutoBlocksMemoSemiStable(text: string, forceCodeWithTitle: string | undefined, forceMarkdown: boolean, forceSanityTextDiffs: SanityTextDiff[] | undefined): RenderBlockInputs {
export function useAutoBlocksMemoSemiStable(text: string, forceCodeWithTitle: string | undefined, forceMarkdown: boolean, forceSanityTextDiffs: SanityTextDiff[] | undefined, selectSingleCodeBlock: boolean): RenderBlockInputs {
// state - previous blocks, to stabilize objects
const prevBlocksRef = React.useRef<RenderBlockInputs>([]);
@@ -75,8 +75,11 @@ export function useAutoBlocksMemoSemiStable(text: string, forceCodeWithTitle: st
newBlocks = [{ bkt: 'md-bk', content: text }];
else if (forceSanityTextDiffs && forceSanityTextDiffs.length >= 1)
newBlocks = [{ bkt: 'txt-diffs-bk', sanityTextDiffs: forceSanityTextDiffs }];
else
else {
newBlocks = parseBlocksFromText(text);
if (selectSingleCodeBlock && newBlocks.length > 1)
newBlocks = newBlocks.filter(({ bkt }) => bkt === 'code-bk');
}
const recycledBlocks: RenderBlockInputs = newBlocks.map((newBlock, index) => {
const prevBlock = prevBlocksRef.current[index] ?? undefined;
+1 -1
View File
@@ -158,7 +158,7 @@ function RenderCodeImpl(props: RenderCodeBaseProps & {
const isMermaidCode = lcBlockTitle === 'mermaid' && !blockIsPartial;
const renderMermaid = isMermaidCode && showMermaid;
const isPlantUMLCode = heuristicIsCodePlantUML(code);
const isPlantUMLCode = heuristicIsCodePlantUML(code.trim());
let renderPlantUML = isPlantUMLCode && showPlantUML;
const { data: plantUmlSvgData, error: plantUmlError } = usePlantUmlSvg(renderPlantUML, code);
renderPlantUML = renderPlantUML && (!!plantUmlSvgData || !!plantUmlError);