mirror of https://github.com/buster-so/buster.git
Merge remote-tracking branch 'origin/staging' into move-search-to-turbo-puffer
This commit is contained in:
commit
c354a08348
|
@ -71,7 +71,8 @@ jobs:
|
|||
run: |
|
||||
echo "📦 Building standalone CLI binary for ${{ matrix.target }}..."
|
||||
# Note: Bun compiles for the host platform, cross-compilation happens via matrix strategy
|
||||
bun build src/index.tsx --compile --outfile dist/buster-cli
|
||||
# Using --minify for production builds to reduce binary size
|
||||
bun build src/index.tsx --compile --minify --outfile dist/buster-cli
|
||||
|
||||
# Make binary executable on Unix systems
|
||||
if [[ "${{ runner.os }}" != "Windows" ]]; then
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Box, Text } from 'ink';
|
||||
import BigText from 'ink-big-text';
|
||||
import React from 'react';
|
||||
import { SimpleBigText } from './simple-big-text.js';
|
||||
|
||||
interface BannerProps {
|
||||
showSubtitle?: boolean;
|
||||
|
@ -8,17 +9,16 @@ interface BannerProps {
|
|||
|
||||
/**
|
||||
* Shared Buster banner component for consistent branding across CLI
|
||||
* Uses SimpleBigText to avoid font loading issues in standalone binaries
|
||||
*/
|
||||
export function BusterBanner({ showSubtitle = true, inline = false }: BannerProps = {}) {
|
||||
const content = (
|
||||
<>
|
||||
<Box>
|
||||
<Text color="#7C3AED">
|
||||
<BigText text="BUSTER" font="block" />
|
||||
</Text>
|
||||
<SimpleBigText text="BUSTER" color="#7C3AED" />
|
||||
</Box>
|
||||
{showSubtitle && (
|
||||
<Box>
|
||||
<Box marginTop={1}>
|
||||
<Text bold>Welcome to Buster</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { Text } from 'ink';
|
||||
import React from 'react';
|
||||
|
||||
interface SimpleBigTextProps {
|
||||
text: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple big text renderer that doesn't require external font files
|
||||
* Uses inline ASCII art for each letter
|
||||
*/
|
||||
export function SimpleBigText({ text, color = 'white' }: SimpleBigTextProps) {
|
||||
const letters: Record<string, string[]> = {
|
||||
B: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '],
|
||||
U: ['██╗ ██╗', '██║ ██║', '██║ ██║', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '],
|
||||
S: ['███████╗', '██╔════╝', '███████╗', '╚════██║', '███████║', '╚══════╝'],
|
||||
T: ['████████╗', '╚══██╔══╝', ' ██║ ', ' ██║ ', ' ██║ ', ' ╚═╝ '],
|
||||
E: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
|
||||
R: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
|
||||
};
|
||||
|
||||
// Convert text to uppercase and get the ASCII art for each letter
|
||||
const upperText = text.toUpperCase();
|
||||
const lines: string[] = ['', '', '', '', '', ''];
|
||||
|
||||
for (const char of upperText) {
|
||||
const letter = letters[char];
|
||||
if (letter) {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
lines[i] += letter[i] + ' ';
|
||||
}
|
||||
} else if (char === ' ') {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
lines[i] += ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Text color={color}>{lines.join('\n')}</Text>;
|
||||
}
|
|
@ -10,7 +10,7 @@ import { InitCommand } from './commands/init.js';
|
|||
program
|
||||
.name('buster')
|
||||
.description('Buster CLI - AI-powered data analytics platform')
|
||||
.version('0.1.0');
|
||||
.version('0.3.0');
|
||||
|
||||
// Auth command - authentication management
|
||||
program
|
||||
|
|
|
@ -367,6 +367,309 @@ describe('messagePostProcessingTask', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Slack notification skip logic', () => {
|
||||
it('should skip Slack notification when no issues found AND no major assumptions', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
const workflowOutput = {
|
||||
flagChatResult: {
|
||||
type: 'noIssuesFound' as const,
|
||||
message: 'No issues found',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
},
|
||||
formattedMessage: undefined,
|
||||
};
|
||||
|
||||
vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({
|
||||
id: messageId,
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
createdAt: new Date(),
|
||||
rawLlmMessages: [] as any,
|
||||
userName: 'John Doe',
|
||||
organizationId: 'org-123',
|
||||
});
|
||||
vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]);
|
||||
vi.mocked(helpers.fetchUserDatasets).mockResolvedValue({
|
||||
datasets: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
vi.mocked(helpers.getExistingSlackMessageForChat).mockResolvedValue({ exists: false });
|
||||
vi.mocked(helpers.buildWorkflowInput).mockReturnValue({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
datasets: '',
|
||||
});
|
||||
vi.mocked(postProcessingWorkflow).mockResolvedValue(workflowOutput);
|
||||
|
||||
await runTask({ messageId });
|
||||
|
||||
// Verify Slack notification was NOT sent
|
||||
expect(helpers.sendSlackNotification).not.toHaveBeenCalled();
|
||||
expect(helpers.sendSlackReplyNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip Slack notification with only minor assumptions', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
const workflowOutput = {
|
||||
flagChatResult: {
|
||||
type: 'noIssuesFound' as const,
|
||||
message: 'No issues found',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Minor assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'minor' as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
formattedMessage: undefined,
|
||||
};
|
||||
|
||||
vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({
|
||||
id: messageId,
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
createdAt: new Date(),
|
||||
rawLlmMessages: [] as any,
|
||||
userName: 'John Doe',
|
||||
organizationId: 'org-123',
|
||||
});
|
||||
vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]);
|
||||
vi.mocked(helpers.fetchUserDatasets).mockResolvedValue({
|
||||
datasets: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
vi.mocked(helpers.getExistingSlackMessageForChat).mockResolvedValue({ exists: false });
|
||||
vi.mocked(helpers.buildWorkflowInput).mockReturnValue({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
datasets: '',
|
||||
});
|
||||
vi.mocked(postProcessingWorkflow).mockResolvedValue(workflowOutput);
|
||||
|
||||
await runTask({ messageId });
|
||||
|
||||
// Verify Slack notification was NOT sent (only minor assumptions)
|
||||
expect(helpers.sendSlackNotification).not.toHaveBeenCalled();
|
||||
expect(helpers.sendSlackReplyNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send Slack notification when flagChat even without major assumptions', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
const workflowOutput = {
|
||||
flagChatResult: {
|
||||
type: 'flagChat' as const,
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Issue Title',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
},
|
||||
formattedMessage: 'Formatted message',
|
||||
};
|
||||
|
||||
vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({
|
||||
id: messageId,
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
createdAt: new Date(),
|
||||
rawLlmMessages: [] as any,
|
||||
userName: 'John Doe',
|
||||
organizationId: 'org-123',
|
||||
});
|
||||
vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]);
|
||||
vi.mocked(helpers.fetchUserDatasets).mockResolvedValue({
|
||||
datasets: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
vi.mocked(helpers.getExistingSlackMessageForChat).mockResolvedValue({ exists: false });
|
||||
vi.mocked(helpers.sendSlackNotification).mockResolvedValue({
|
||||
sent: true,
|
||||
messageTs: 'msg-ts-123',
|
||||
integrationId: 'int-123',
|
||||
channelId: 'C123456',
|
||||
});
|
||||
vi.mocked(helpers.buildWorkflowInput).mockReturnValue({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
datasets: '',
|
||||
});
|
||||
vi.mocked(postProcessingWorkflow).mockResolvedValue(workflowOutput);
|
||||
|
||||
await runTask({ messageId });
|
||||
|
||||
// Verify Slack notification WAS sent (flagChat type)
|
||||
expect(helpers.sendSlackNotification).toHaveBeenCalledWith({
|
||||
organizationId: 'org-123',
|
||||
userName: 'John Doe',
|
||||
chatId: 'chat-123',
|
||||
summaryTitle: 'Issue Title',
|
||||
summaryMessage: 'Issues found',
|
||||
toolCalled: 'noAssumptions',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send Slack notification when major assumptions exist even with noIssuesFound', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
const workflowOutput = {
|
||||
flagChatResult: {
|
||||
type: 'noIssuesFound' as const,
|
||||
message: 'No issues',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test',
|
||||
label: 'major' as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
formattedMessage: 'Major assumptions found',
|
||||
};
|
||||
|
||||
vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({
|
||||
id: messageId,
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
createdAt: new Date(),
|
||||
rawLlmMessages: [] as any,
|
||||
userName: 'John Doe',
|
||||
organizationId: 'org-123',
|
||||
});
|
||||
vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]);
|
||||
vi.mocked(helpers.fetchUserDatasets).mockResolvedValue({
|
||||
datasets: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
vi.mocked(helpers.getExistingSlackMessageForChat).mockResolvedValue({ exists: false });
|
||||
vi.mocked(helpers.sendSlackNotification).mockResolvedValue({
|
||||
sent: true,
|
||||
messageTs: 'msg-ts-123',
|
||||
integrationId: 'int-123',
|
||||
channelId: 'C123456',
|
||||
});
|
||||
vi.mocked(helpers.buildWorkflowInput).mockReturnValue({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
datasets: '',
|
||||
});
|
||||
vi.mocked(postProcessingWorkflow).mockResolvedValue(workflowOutput);
|
||||
|
||||
await runTask({ messageId });
|
||||
|
||||
// Verify Slack notification WAS sent (major assumptions exist)
|
||||
expect(helpers.sendSlackNotification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle undefined isSlackFollowUp in workflow input correctly', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
|
||||
const workflowOutput = {
|
||||
flagChatResult: {
|
||||
type: 'flagChat' as const,
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Issue Title',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test',
|
||||
label: 'major' as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
formattedMessage: 'Formatted message from workflow',
|
||||
};
|
||||
|
||||
vi.mocked(helpers.fetchMessageWithContext).mockResolvedValue({
|
||||
id: messageId,
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
createdAt: new Date(),
|
||||
rawLlmMessages: [] as any,
|
||||
userName: 'John Doe',
|
||||
organizationId: 'org-123',
|
||||
});
|
||||
vi.mocked(helpers.fetchPreviousPostProcessingMessages).mockResolvedValue([]);
|
||||
vi.mocked(helpers.fetchUserDatasets).mockResolvedValue({
|
||||
datasets: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 1000,
|
||||
});
|
||||
vi.mocked(helpers.getExistingSlackMessageForChat).mockResolvedValue({ exists: false });
|
||||
vi.mocked(helpers.sendSlackNotification).mockResolvedValue({
|
||||
sent: true,
|
||||
messageTs: 'msg-ts-123',
|
||||
integrationId: 'int-123',
|
||||
channelId: 'C123456',
|
||||
});
|
||||
vi.mocked(helpers.buildWorkflowInput).mockReturnValue({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: undefined, // Critical: undefined case
|
||||
datasets: '',
|
||||
});
|
||||
vi.mocked(postProcessingWorkflow).mockResolvedValue(workflowOutput);
|
||||
|
||||
await runTask({ messageId });
|
||||
|
||||
// Verify workflow was called with undefined isSlackFollowUp
|
||||
expect(postProcessingWorkflow).toHaveBeenCalledWith({
|
||||
conversationHistory: undefined,
|
||||
userName: 'John Doe',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: undefined,
|
||||
datasets: '',
|
||||
});
|
||||
|
||||
// Verify Slack notification WAS sent with formatted message
|
||||
expect(helpers.sendSlackNotification).toHaveBeenCalledWith({
|
||||
organizationId: 'org-123',
|
||||
userName: 'John Doe',
|
||||
chatId: 'chat-123',
|
||||
summaryTitle: 'Issue Title',
|
||||
summaryMessage: 'Issues found',
|
||||
toolCalled: 'listAssumptions',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error result for database update failure', async () => {
|
||||
const messageId = '123e4567-e89b-12d3-a456-426614174000';
|
||||
const dbError = new Error('Database update failed');
|
||||
|
|
|
@ -234,11 +234,11 @@ export const messagePostProcessingTask: ReturnType<
|
|||
// Step 7: Send Slack notification if conditions are met
|
||||
let slackNotificationSent = false;
|
||||
|
||||
// Skip Slack notification if tool_called is "noIssuesFound" and there are no major assumptions
|
||||
// Skip Slack notification if no issues were flagged and there are no major assumptions
|
||||
const hasMajorAssumptions =
|
||||
dbData.assumptions?.some((assumption) => assumption.label === 'major') ?? false;
|
||||
const shouldSkipSlackNotification =
|
||||
dbData.tool_called === 'noIssuesFound' && !hasMajorAssumptions;
|
||||
validatedOutput.flagChatResult.type === 'noIssuesFound' && !hasMajorAssumptions;
|
||||
|
||||
try {
|
||||
logger.log('Checking Slack notification conditions', {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { useListReportVersionDropdownItems } from '@/components/features/version
|
|||
import { Button } from '@/components/ui/buttons';
|
||||
import {
|
||||
createDropdownItem,
|
||||
createDropdownItems,
|
||||
Dropdown,
|
||||
DropdownContent,
|
||||
type IDropdownItem,
|
||||
|
@ -23,12 +24,20 @@ import {
|
|||
} from '@/components/ui/dropdown';
|
||||
import { Dots, History, PenSparkle, ShareRight, Star } from '@/components/ui/icons';
|
||||
import { Star as StarFilled } from '@/components/ui/icons/NucleoIconFilled';
|
||||
import { Download4, Refresh } from '@/components/ui/icons/NucleoIconOutlined';
|
||||
import {
|
||||
Download4,
|
||||
DuplicatePlus,
|
||||
Redo,
|
||||
Refresh,
|
||||
Undo,
|
||||
} from '@/components/ui/icons/NucleoIconOutlined';
|
||||
import { useStartChatFromAsset } from '@/context/BusterAssets/useStartChatFromAsset';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { useGetChatId } from '@/context/Chats/useGetChatId';
|
||||
import { useReportPageExport } from '@/context/Reports/useReportPageExport';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { useIsMac } from '@/hooks/usePlatform';
|
||||
import { useEditorContext } from '@/layouts/AssetContainer/ReportAssetContainer';
|
||||
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
|
||||
|
||||
export const ReportThreeDotMenu = React.memo(
|
||||
|
@ -46,9 +55,10 @@ export const ReportThreeDotMenu = React.memo(
|
|||
const saveToLibrary = useSaveToLibrary({ reportId });
|
||||
const favoriteItem = useFavoriteReportSelectMenu({ reportId });
|
||||
const versionHistory = useVersionHistorySelectMenu({ reportId });
|
||||
const undoRedo = useUndoRedo();
|
||||
const duplicateReport = useDuplicateReportSelectMenu({ reportId });
|
||||
// const verificationItem = useReportVerificationSelectMenu(); // Hidden - not supported yet
|
||||
const refreshReportItem = useRefreshReportSelectMenu({ reportId });
|
||||
// const duplicateReportItem = useDuplicateReportSelectMenu();
|
||||
const { dropdownItem: downloadPdfItem, exportPdfContainer } = useDownloadPdfSelectMenu({
|
||||
reportId,
|
||||
});
|
||||
|
@ -68,11 +78,13 @@ export const ReportThreeDotMenu = React.memo(
|
|||
saveToLibrary,
|
||||
favoriteItem,
|
||||
{ type: 'divider' },
|
||||
...undoRedo,
|
||||
{ type: 'divider' },
|
||||
versionHistory,
|
||||
// verificationItem, // Hidden - not supported yet
|
||||
{ type: 'divider' },
|
||||
isEditor && refreshReportItem,
|
||||
// duplicateReportItem,
|
||||
duplicateReport,
|
||||
downloadPdfItem,
|
||||
];
|
||||
}, [
|
||||
|
@ -293,7 +305,6 @@ const useReportVerificationSelectMenu = (): IDropdownItem => {
|
|||
|
||||
// Refresh report with latest data
|
||||
const useRefreshReportSelectMenu = ({ reportId }: { reportId: string }): IDropdownItem => {
|
||||
const navigate = useNavigate();
|
||||
const { onCreateFileClick, loading: isPending } = useStartChatFromAsset({
|
||||
assetId: reportId,
|
||||
assetType: 'report',
|
||||
|
@ -374,3 +385,55 @@ const useDownloadPdfSelectMenu = ({
|
|||
};
|
||||
}, [reportId, exportReportAsPDF, cancelExport, ExportContainer]);
|
||||
};
|
||||
|
||||
const useUndoRedo = (): IDropdownItems => {
|
||||
const { editor } = useEditorContext();
|
||||
const isMac = useIsMac();
|
||||
const getEditor = () => {
|
||||
if (!editor?.current) {
|
||||
console.warn('Editor is not defined');
|
||||
return;
|
||||
}
|
||||
return editor?.current;
|
||||
};
|
||||
return useMemo(
|
||||
() =>
|
||||
createDropdownItems([
|
||||
{
|
||||
label: 'Undo',
|
||||
value: 'undo',
|
||||
shortcut: isMac ? '⌘+Z' : 'Ctrl+Z',
|
||||
icon: <Undo />,
|
||||
onClick: () => {
|
||||
const editorInstance = getEditor();
|
||||
editorInstance?.undo();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
value: 'redo',
|
||||
shortcut: isMac ? '⌘+⇧+Z' : 'Ctrl+⇧+Z',
|
||||
icon: <Redo />,
|
||||
onClick: () => {
|
||||
const editorInstance = getEditor();
|
||||
editorInstance?.redo();
|
||||
},
|
||||
},
|
||||
]),
|
||||
[isMac]
|
||||
);
|
||||
};
|
||||
|
||||
const useDuplicateReportSelectMenu = ({ reportId }: { reportId: string }): IDropdownItem => {
|
||||
return useMemo(
|
||||
() => ({
|
||||
label: 'Duplicate',
|
||||
value: 'duplicate-report',
|
||||
icon: <DuplicatePlus />,
|
||||
onClick: () => {
|
||||
console.log('Duplicate report');
|
||||
},
|
||||
}),
|
||||
[reportId]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -99,7 +99,3 @@ export const EditorKit = ({
|
|||
// Dnd
|
||||
...DndKit({ containerRef }),
|
||||
];
|
||||
|
||||
export type MyEditor = TPlateEditor<Value, ReturnType<typeof EditorKit>[number]>;
|
||||
|
||||
export const useEditor = () => useEditorRef<MyEditor>();
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import type { Value } from 'platejs';
|
||||
import type { TPlateEditor } from 'platejs/react';
|
||||
import type { EditorKit } from './editor-kit';
|
||||
|
||||
export type BusterReportEditor = TPlateEditor<Value, ReturnType<typeof EditorKit>[number]>;
|
|
@ -0,0 +1,4 @@
|
|||
import { useEditorRef } from 'platejs/react';
|
||||
import type { BusterReportEditor } from './types';
|
||||
|
||||
export const useEditor = () => useEditorRef<BusterReportEditor>();
|
|
@ -3,7 +3,7 @@ import type { QueryClient } from '@tanstack/react-query';
|
|||
import { Outlet } from '@tanstack/react-router';
|
||||
import { z } from 'zod';
|
||||
import { prefetchGetReport } from '@/api/buster_rest/reports';
|
||||
import { ReportAssetContainer } from '../../../layouts/AssetContainer/ReportAssetContainer/ReportAssetContainer';
|
||||
import { ReportAssetContainer } from '@/layouts/AssetContainer/ReportAssetContainer/ReportAssetContainer';
|
||||
import { useGetReportParams } from '../../Reports/useGetReportParams';
|
||||
|
||||
export const validateSearch = z.object({
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ShimmerText } from '@/components/ui/typography/ShimmerText';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const GeneratingContent = ({ className }: { messageId: string; className?: string }) => {
|
||||
export const GeneratingContent = ({
|
||||
className,
|
||||
show,
|
||||
}: {
|
||||
messageId: string;
|
||||
className?: string;
|
||||
show: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('right-0 bottom-0 left-0 -mt-68', className)}>
|
||||
<ShimmerText text="Generating..." className="text-lg" />
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{show && (
|
||||
<motion.div
|
||||
className={cn('right-0 bottom-0 left-0 absolute translate-y-[-255px]', className)}
|
||||
initial={{ opacity: 0, y: -3 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -3 }}
|
||||
transition={{ duration: 0.12, delay: 0.15 }}
|
||||
>
|
||||
<ShimmerText text="Generating..." fontSize={15} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,14 +6,18 @@ import { useTrackAndUpdateReportChanges } from '@/api/buster-electric/reports/ho
|
|||
import DynamicReportEditor from '@/components/ui/report/DynamicReportEditor';
|
||||
import type { IReportEditor } from '@/components/ui/report/ReportEditor';
|
||||
import { ReportEditorSkeleton } from '@/components/ui/report/ReportEditorSkeleton';
|
||||
import type { BusterReportEditor } from '@/components/ui/report/types';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { useMount } from '@/hooks/useMount';
|
||||
import { useEditorContext } from '@/layouts/AssetContainer/ReportAssetContainer';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { chatQueryKeys } from '../../api/query_keys/chat';
|
||||
import { useGetCurrentMessageId, useIsStreamingMessage } from '../../context/Chats';
|
||||
import { GeneratingContent } from './GeneratingContent';
|
||||
import { ReportPageHeader } from './ReportPageHeader';
|
||||
|
||||
const commonClassName = 'sm:px-[max(64px,calc(50%-350px))]';
|
||||
|
||||
export const ReportPageController: React.FC<{
|
||||
reportId: string;
|
||||
readOnly?: boolean;
|
||||
|
@ -23,6 +27,7 @@ export const ReportPageController: React.FC<{
|
|||
}> = React.memo(
|
||||
({ reportId, readOnly = false, className = '', onReady: onReadyProp, mode = 'default' }) => {
|
||||
const { data: report } = useGetReport({ id: reportId, versionNumber: undefined });
|
||||
const { setEditor } = useEditorContext();
|
||||
const isStreamingMessage = useIsStreamingMessage();
|
||||
const messageId = useGetCurrentMessageId();
|
||||
|
||||
|
@ -45,7 +50,6 @@ export const ReportPageController: React.FC<{
|
|||
|
||||
const content = report?.content || '';
|
||||
const showGeneratingContent = isThisReportBeingGenerated;
|
||||
const commonClassName = 'sm:px-[max(64px,calc(50%-350px))]';
|
||||
|
||||
const { mutate: updateReport } = useUpdateReport();
|
||||
|
||||
|
@ -71,6 +75,11 @@ export const ReportPageController: React.FC<{
|
|||
updateReport({ reportId, content });
|
||||
});
|
||||
|
||||
const onReady = useMemoizedFn((editor: BusterReportEditor) => {
|
||||
setEditor(editor);
|
||||
onReadyProp?.(editor);
|
||||
});
|
||||
|
||||
useTrackAndUpdateReportChanges({ reportId, subscribe: isStreamingMessage });
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -102,7 +111,7 @@ export const ReportPageController: React.FC<{
|
|||
onValueChange={onChangeContent}
|
||||
readOnly={readOnly || !report}
|
||||
mode={mode}
|
||||
onReady={onReadyProp}
|
||||
onReady={onReady}
|
||||
isStreaming={isStreamingMessage}
|
||||
containerRef={containerRef}
|
||||
preEditorChildren={
|
||||
|
@ -115,9 +124,11 @@ export const ReportPageController: React.FC<{
|
|||
/>
|
||||
}
|
||||
postEditorChildren={
|
||||
showGeneratingContent ? (
|
||||
<GeneratingContent messageId={messageId || ''} className={commonClassName} />
|
||||
) : null
|
||||
<GeneratingContent
|
||||
messageId={messageId || ''}
|
||||
className={commonClassName}
|
||||
show={showGeneratingContent}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { useMemo } from 'react';
|
||||
import { isServer } from '@/lib/window';
|
||||
|
||||
/**
|
||||
* Hook to detect if the user is on a Mac system
|
||||
* @returns {boolean} true if the user is on a Mac, false otherwise (Windows, Linux, etc.)
|
||||
*/
|
||||
export const useIsMac = (): boolean => {
|
||||
return useMemo(() => {
|
||||
if (isServer) {
|
||||
return false; // Server-side rendering fallback
|
||||
}
|
||||
|
||||
// Check the user agent for Mac indicators
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
|
||||
return userAgent.includes('macintosh') || userAgent.includes('mac os');
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to detect if the user is on a Windows system
|
||||
* @returns {boolean} true if the user is on Windows, false otherwise
|
||||
*/
|
||||
export const useIsWindows = (): boolean => {
|
||||
return useMemo(() => {
|
||||
if (isServer) {
|
||||
return false; // Server-side rendering fallback
|
||||
}
|
||||
|
||||
// Check the user agent for Windows indicators
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('windows');
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get the platform type
|
||||
* @returns {'mac' | 'windows' | 'other'} the detected platform
|
||||
*/
|
||||
export const usePlatform = (): 'mac' | 'windows' | 'other' => {
|
||||
return useMemo(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'other'; // Server-side rendering fallback
|
||||
}
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.includes('macintosh') || userAgent.includes('mac os')) {
|
||||
return 'mac';
|
||||
}
|
||||
|
||||
if (userAgent.includes('windows')) {
|
||||
return 'windows';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useBrowser = (): 'chrome' | 'firefox' | 'safari' | 'edge' | 'other' => {
|
||||
return useMemo(() => {
|
||||
if (isServer) {
|
||||
return 'other'; // Server-side rendering fallback
|
||||
}
|
||||
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.includes('chrome')) {
|
||||
return 'chrome';
|
||||
}
|
||||
|
||||
if (userAgent.includes('firefox')) {
|
||||
return 'firefox';
|
||||
}
|
||||
|
||||
if (userAgent.includes('safari')) {
|
||||
return 'safari';
|
||||
}
|
||||
|
||||
if (userAgent.includes('edge')) {
|
||||
return 'edge';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}, []);
|
||||
};
|
|
@ -39,7 +39,7 @@ const LazyMetricStoreDevtools = !import.meta.env.SSR
|
|||
// The actual devtools component implementation
|
||||
const TanstackDevtoolsImpl: React.FC = React.memo(() => {
|
||||
useMount(() => {
|
||||
console.log('🐓 Rendering TanstackDevtoolsImpl');
|
||||
if (import.meta.env.PROD) console.log('🐓 Rendering TanstackDevtoolsImpl');
|
||||
});
|
||||
const isServerOrSSR = isServer && import.meta.env.SSR;
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import { useGetReport } from '@/api/buster_rest/reports';
|
|||
import { CreateChatButton } from '@/components/features/AssetLayout/CreateChatButton';
|
||||
import { ShareReportButton } from '@/components/features/buttons/ShareReportButton';
|
||||
import { ClosePageButton } from '@/components/features/chat/ClosePageButton';
|
||||
import { ReportThreeDotMenu } from '@/components/features/reports/ReportThreeDotMenu';
|
||||
import { useIsChatMode, useIsFileMode } from '@/context/Chats/useMode';
|
||||
import { useIsReportReadOnly } from '@/context/Reports/useIsReportReadOnly';
|
||||
import { getIsEffectiveOwner } from '@/lib/share';
|
||||
import { ReportThreeDotMenu } from '../../../components/features/reports/ReportThreeDotMenu';
|
||||
import { FileButtonContainer } from '../FileButtonContainer';
|
||||
import { HideButtonContainer } from '../HideButtonContainer';
|
||||
|
||||
|
@ -26,7 +26,7 @@ export const ReportContainerHeaderButtons: React.FC<ReportContainerHeaderButtons
|
|||
const { isViewingOldVersion } = useIsReportReadOnly({
|
||||
reportId: reportId || '',
|
||||
});
|
||||
const { error: reportError, data: permission } = useGetReport(
|
||||
const { data: permission } = useGetReport(
|
||||
{ id: reportId },
|
||||
{ select: useCallback((x: GetReportResponse) => x.permission, []) }
|
||||
);
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { useState } from 'react';
|
||||
import { useCallback, useRef, useState, useTransition } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import type { BusterReportEditor } from '@/components/ui/report/types';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
|
||||
const useReportAssetContext = () => {
|
||||
const [forceUpdate, startForceUpdate] = useTransition();
|
||||
const [versionHistoryMode, setVersionHistoryMode] = useState<number | false>(false);
|
||||
const editor = useRef<BusterReportEditor | null>(null);
|
||||
|
||||
const openReportVersionHistoryMode = useMemoizedFn((versionNumber: number) => {
|
||||
setVersionHistoryMode(versionNumber);
|
||||
|
@ -13,10 +16,23 @@ const useReportAssetContext = () => {
|
|||
setVersionHistoryMode(false);
|
||||
});
|
||||
|
||||
const setEditor = useMemoizedFn((editorInstance: BusterReportEditor) => {
|
||||
if (!editorInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
startForceUpdate(() => {
|
||||
editor.current = editorInstance;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
openReportVersionHistoryMode,
|
||||
closeVersionHistoryMode,
|
||||
versionHistoryMode,
|
||||
setEditor,
|
||||
forceUpdate,
|
||||
editor,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -55,3 +71,25 @@ export const useReportVersionHistoryMode = () => {
|
|||
closeVersionHistoryMode,
|
||||
};
|
||||
};
|
||||
|
||||
const stableSetEditorSelector = (x: ReturnType<typeof useReportAssetContext>) => x.setEditor;
|
||||
const stableForceUpdateSelector = (x: ReturnType<typeof useReportAssetContext>) => x.forceUpdate;
|
||||
export const useEditorContext = () => {
|
||||
const forceUpdate = useContextSelector(ReportAssetContext, stableForceUpdateSelector);
|
||||
const setEditor = useContextSelector(ReportAssetContext, stableSetEditorSelector);
|
||||
const editor = useContextSelector(
|
||||
ReportAssetContext,
|
||||
useCallback((x) => x.editor, [forceUpdate])
|
||||
);
|
||||
|
||||
if (!setEditor) {
|
||||
console.warn(
|
||||
'ReportAssetContext is not defined. useEditorContext must be used within a ReportAssetContextProvider.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
editor,
|
||||
setEditor,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,419 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { runMessagePostProcessingWorkflow } from './message-post-processing-workflow';
|
||||
import type { PostProcessingWorkflowInput } from './message-post-processing-workflow';
|
||||
|
||||
// Mock the steps
|
||||
vi.mock('../../steps/message-post-processing-steps/flag-chat-step/flag-chat-step', () => ({
|
||||
runFlagChatStep: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
'../../steps/message-post-processing-steps/identify-assumptions-step/identify-assumptions-step',
|
||||
() => ({
|
||||
runIdentifyAssumptionsStep: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'../../steps/message-post-processing-steps/format-follow-up-message-step/format-follow-up-message-step',
|
||||
() => ({
|
||||
runFormatFollowUpMessageStep: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'../../steps/message-post-processing-steps/format-initial-message-step/format-initial-message-step',
|
||||
() => ({
|
||||
runFormatInitialMessageStep: vi.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
import { runFlagChatStep } from '../../steps/message-post-processing-steps/flag-chat-step/flag-chat-step';
|
||||
import { runFormatFollowUpMessageStep } from '../../steps/message-post-processing-steps/format-follow-up-message-step/format-follow-up-message-step';
|
||||
import { runFormatInitialMessageStep } from '../../steps/message-post-processing-steps/format-initial-message-step/format-initial-message-step';
|
||||
import { runIdentifyAssumptionsStep } from '../../steps/message-post-processing-steps/identify-assumptions-step/identify-assumptions-step';
|
||||
|
||||
describe('runMessagePostProcessingWorkflow', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Formatting logic edge cases', () => {
|
||||
it('should format initial message when isSlackFollowUp is undefined', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: undefined, // Critical test case
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'flagChat',
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Test Issue',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
vi.mocked(runFormatInitialMessageStep).mockResolvedValue({
|
||||
summaryMessage: 'Formatted initial message',
|
||||
summaryTitle: 'Initial Title',
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify - should call format initial message when isSlackFollowUp is undefined
|
||||
expect(runFormatInitialMessageStep).toHaveBeenCalledWith({
|
||||
userName: 'Test User',
|
||||
flaggedIssues: 'Issues found',
|
||||
majorAssumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
conversationHistory: undefined,
|
||||
});
|
||||
|
||||
expect(result.formattedMessage).toBe('Formatted initial message');
|
||||
});
|
||||
|
||||
it('should format initial message when isSlackFollowUp is false', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false, // Explicit false
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'flagChat',
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Test Issue',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
vi.mocked(runFormatInitialMessageStep).mockResolvedValue({
|
||||
summaryMessage: 'Formatted initial message',
|
||||
summaryTitle: 'Initial Title',
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify
|
||||
expect(runFormatInitialMessageStep).toHaveBeenCalled();
|
||||
expect(result.formattedMessage).toBe('Formatted initial message');
|
||||
});
|
||||
|
||||
it('should format follow-up message when both isFollowUp and isSlackFollowUp are true', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: true,
|
||||
isSlackFollowUp: true,
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'flagChat',
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Test Issue',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
vi.mocked(runFormatFollowUpMessageStep).mockResolvedValue({
|
||||
summaryMessage: 'Formatted follow-up message',
|
||||
summaryTitle: 'Follow-up Title',
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify
|
||||
expect(runFormatFollowUpMessageStep).toHaveBeenCalled();
|
||||
expect(runFormatInitialMessageStep).not.toHaveBeenCalled();
|
||||
expect(result.formattedMessage).toBe('Formatted follow-up message');
|
||||
});
|
||||
|
||||
it('should NOT format message when no major assumptions and no flagged issues', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'No issues found',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify - no formatting functions should be called
|
||||
expect(runFormatInitialMessageStep).not.toHaveBeenCalled();
|
||||
expect(runFormatFollowUpMessageStep).not.toHaveBeenCalled();
|
||||
expect(result.formattedMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should format message when major assumptions exist even without flagged issues', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'No issues found',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Major assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
vi.mocked(runFormatInitialMessageStep).mockResolvedValue({
|
||||
summaryMessage: 'Major assumptions require attention',
|
||||
summaryTitle: 'Assumptions Found',
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify - should format because of major assumptions
|
||||
expect(runFormatInitialMessageStep).toHaveBeenCalled();
|
||||
expect(result.formattedMessage).toBe('Major assumptions require attention');
|
||||
});
|
||||
|
||||
it('should NOT format message with only minor assumptions', async () => {
|
||||
// Setup
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
isFollowUp: false,
|
||||
isSlackFollowUp: false,
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'No issues found',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Minor assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'minor',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
// Verify - should NOT format because only minor assumptions
|
||||
expect(runFormatInitialMessageStep).not.toHaveBeenCalled();
|
||||
expect(runFormatFollowUpMessageStep).not.toHaveBeenCalled();
|
||||
expect(result.formattedMessage).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Result structure validation', () => {
|
||||
it('should return correct structure for flagChat with assumptions', async () => {
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'flagChat',
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Test Issue',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Test assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
vi.mocked(runFormatInitialMessageStep).mockResolvedValue({
|
||||
summaryMessage: 'Formatted message',
|
||||
summaryTitle: 'Title',
|
||||
});
|
||||
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
flagChatResult: {
|
||||
type: 'flagChat',
|
||||
summaryMessage: 'Issues found',
|
||||
summaryTitle: 'Test Issue',
|
||||
message: undefined,
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'listAssumptions',
|
||||
assumptions: [
|
||||
{
|
||||
descriptiveTitle: 'Test assumption',
|
||||
classification: 'fieldMapping',
|
||||
explanation: 'Test explanation',
|
||||
label: 'major',
|
||||
},
|
||||
],
|
||||
},
|
||||
formattedMessage: 'Formatted message',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct structure for noIssuesFound with no assumptions', async () => {
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: 'test datasets',
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'All good',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
});
|
||||
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
flagChatResult: {
|
||||
type: 'noIssuesFound',
|
||||
summaryMessage: undefined,
|
||||
summaryTitle: undefined,
|
||||
message: 'All good',
|
||||
},
|
||||
assumptionsResult: {
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
},
|
||||
formattedMessage: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle all undefined optional fields', async () => {
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: '',
|
||||
conversationHistory: undefined,
|
||||
isFollowUp: undefined,
|
||||
isSlackFollowUp: undefined,
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'No issues',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.formattedMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty datasets string', async () => {
|
||||
const input: PostProcessingWorkflowInput = {
|
||||
userName: 'Test User',
|
||||
datasets: '',
|
||||
};
|
||||
|
||||
vi.mocked(runFlagChatStep).mockResolvedValue({
|
||||
type: 'noIssuesFound',
|
||||
message: 'No issues',
|
||||
});
|
||||
|
||||
vi.mocked(runIdentifyAssumptionsStep).mockResolvedValue({
|
||||
toolCalled: 'noAssumptions',
|
||||
assumptions: undefined,
|
||||
});
|
||||
|
||||
const result = await runMessagePostProcessingWorkflow(input);
|
||||
|
||||
expect(runFlagChatStep).toHaveBeenCalledWith({
|
||||
conversationHistory: undefined,
|
||||
userName: 'Test User',
|
||||
datasets: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -91,8 +91,8 @@ export async function runMessagePostProcessingWorkflow(
|
|||
conversationHistory: validatedInput.conversationHistory,
|
||||
});
|
||||
formattedMessage = followUpResult.summaryMessage;
|
||||
} else if (validatedInput.isSlackFollowUp === false) {
|
||||
// Format initial message
|
||||
} else if (!validatedInput.isSlackFollowUp) {
|
||||
// Format initial message (when isSlackFollowUp is false or undefined)
|
||||
const initialResult = await runFormatInitialMessageStep({
|
||||
userName: validatedInput.userName,
|
||||
flaggedIssues,
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.ts",
|
||||
"default": "./dist/types.js"
|
||||
},
|
||||
"./helpers/*": {
|
||||
"types": "./dist/helpers/*/index.d.ts",
|
||||
"default": "./dist/helpers/*/index.js"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Type-only exports for message schemas
|
||||
* This file provides types without triggering database connection
|
||||
*/
|
||||
|
||||
// Export message schema types
|
||||
export type {
|
||||
ChatMessageReasoning_status,
|
||||
ChatMessageResponseMessage,
|
||||
ChatMessageReasoningMessage,
|
||||
ChatMessageReasoningMessage_Text,
|
||||
ChatMessageReasoningMessage_Files,
|
||||
ChatMessageReasoningMessage_Pills,
|
||||
ChatMessageReasoningMessage_File,
|
||||
ChatMessageReasoningMessage_Pill,
|
||||
ChatMessageReasoningMessage_PillContainer,
|
||||
ChatMessageResponseMessage_FileMetadata,
|
||||
ChatMessageResponseMessage_Text,
|
||||
ChatMessageResponseMessage_File,
|
||||
ReasoningFileType,
|
||||
ResponseMessageFileType,
|
||||
ReasoingMessage_ThoughtFileType,
|
||||
} from './schemas/message-schemas';
|
||||
|
||||
// Export the schemas themselves (these are just objects, no side effects)
|
||||
export {
|
||||
StatusSchema,
|
||||
ResponseMessageSchema,
|
||||
ReasoningMessageSchema,
|
||||
} from './schemas/message-schemas';
|
|
@ -6,6 +6,7 @@ export * from './requests';
|
|||
export * from './responses';
|
||||
|
||||
// Re-export message schemas from database package to maintain backward compatibility
|
||||
// Using /types entry point to avoid triggering database connection
|
||||
export {
|
||||
StatusSchema,
|
||||
ResponseMessageSchema,
|
||||
|
@ -25,4 +26,4 @@ export {
|
|||
type ReasoningFileType,
|
||||
type ResponseMessageFileType,
|
||||
type ReasoingMessage_ThoughtFileType,
|
||||
} from '@buster/database';
|
||||
} from '@buster/database/types';
|
||||
|
|
Loading…
Reference in New Issue