Skip to content

Commit 1c3beb7

Browse files
committed
WIP AI stuff
1 parent d25e786 commit 1c3beb7

File tree

11 files changed

+1535
-57
lines changed

11 files changed

+1535
-57
lines changed

browser/data-browser/src/components/AI/AIChatMessage.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import styled from 'styled-components';
22
import Markdown from '../datatypes/Markdown';
33
import type { CoreAssistantMessage, CoreToolMessage } from 'ai';
4-
import { FaCircleExclamation, FaTrash } from 'react-icons/fa6';
4+
import { FaCircleExclamation, FaRetweet, FaTrash } from 'react-icons/fa6';
55
import {
66
isAIErrorMessage,
77
isMessageWithContext,
@@ -14,6 +14,7 @@ import { IconButton } from '../IconButton/IconButton';
1414
interface MessageProps {
1515
message: AIChatDisplayMessage;
1616
onDeleteMessage?: (message: AIChatDisplayMessage) => void;
17+
onRegenerateMessage?: (message: AIChatDisplayMessage) => void;
1718
}
1819

1920
function isToolMessage(
@@ -31,14 +32,19 @@ function isAssistantMessage(
3132
export const AIChatMessage = ({
3233
message: messageIn,
3334
onDeleteMessage,
35+
onRegenerateMessage,
3436
}: MessageProps) => {
3537
const [message, context] = isMessageWithContext(messageIn)
3638
? [messageIn.message, messageIn.context]
3739
: [messageIn];
3840

3941
if (message.role === 'user') {
4042
return (
41-
<MessageActionWrapper message={message} onDeleteMessage={onDeleteMessage}>
43+
<MessageActionWrapper
44+
message={message}
45+
onDeleteMessage={onDeleteMessage}
46+
onRegenerateMessage={onRegenerateMessage}
47+
>
4248
<UserMessage message={message} context={context} />
4349
</MessageActionWrapper>
4450
);
@@ -116,20 +122,30 @@ const MessageActionWrapper: React.FC<React.PropsWithChildren<MessageProps>> = ({
116122
children,
117123
message,
118124
onDeleteMessage,
125+
onRegenerateMessage,
119126
}) => {
120127
return (
121128
<MessageTopWrapper>
122-
{onDeleteMessage && (
123-
<FloatingActionRow>
129+
<FloatingActionRow>
130+
{onDeleteMessage && (
124131
<IconButton
125132
color='textLight'
126133
onClick={() => onDeleteMessage(message)}
127134
title='Delete Message'
128135
>
129136
<FaTrash />
130137
</IconButton>
131-
</FloatingActionRow>
132-
)}
138+
)}
139+
{onRegenerateMessage && (
140+
<IconButton
141+
color='textLight'
142+
onClick={() => onRegenerateMessage(message)}
143+
title='Regenerate response'
144+
>
145+
<FaRetweet />
146+
</IconButton>
147+
)}
148+
</FloatingActionRow>
133149
{children}
134150
</MessageTopWrapper>
135151
);

browser/data-browser/src/components/AI/AISettings.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { OpenRouterLoginButton } from './OpenRouterLoginButton';
1111
import { TabPanel, Tabs } from '../Tabs';
1212
import { effectFetch } from '../../helpers/effectFetch';
1313

14+
const intl = new Intl.NumberFormat('default', {
15+
style: 'currency',
16+
currency: 'USD',
17+
minimumFractionDigits: 2,
18+
maximumFractionDigits: 2,
19+
});
20+
1421
interface CreditUsage {
1522
total: number;
1623
used: number;
@@ -99,8 +106,8 @@ const AISettings: React.FC = () => {
99106
</Row>
100107
{creditUsage && (
101108
<Subtle as='p'>
102-
Credits used: {creditUsage.used} / Total:{' '}
103-
{creditUsage.total}
109+
Credits used: {intl.format(creditUsage.used)} /{' '}
110+
{intl.format(creditUsage.total)}
104111
</Subtle>
105112
)}
106113
{!openRouterApiKey && (

browser/data-browser/src/components/AI/AgentConfig.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ const AgentForm = ({ agent, onChange }: AgentFormProps) => {
368368
<Label htmlFor='agent-name'>Name</Label>
369369
<Input
370370
id='agent-name'
371+
required
371372
max={50}
372373
value={agent.name}
373374
onChange={e => handleChange('name', e.target.value)}

browser/data-browser/src/components/AI/SimpleAIChat.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { NoKeyOverlay } from './NoKeyOverlay';
4747
import { useAutoAgentSelect } from './useAgentAutoSelect';
4848
import { useOpenRouterModels } from './useOpenRouterModels';
4949
import type { MentionItem } from '../../chunks/MarkdownEditor/AIChatInput/types';
50+
import { flushSync } from 'react-dom';
5051

5152
const AIChatInput = React.lazy(
5253
() => import('../../chunks/MarkdownEditor/AIChatInput/AsyncAIChatInput'),
@@ -82,6 +83,7 @@ interface SimpleAIChatProps {
8283
React.SetStateAction<AIMessageContext[]>
8384
>;
8485
onDeleteMessage: (message: AIChatDisplayMessage) => void;
86+
onRegenerateMessage: (message: AIChatDisplayMessage) => Promise<void>;
8587
}
8688

8789
export const SimpleAIChat: React.FC<
@@ -94,6 +96,7 @@ export const SimpleAIChat: React.FC<
9496
setExternalContextItems,
9597
onNewMessage,
9698
onDeleteMessage,
99+
onRegenerateMessage,
97100
children,
98101
}) => {
99102
const abortSignalRef = useRef<AbortController>(null);
@@ -140,11 +143,12 @@ export const SimpleAIChat: React.FC<
140143
const [tokensUsed, setTokensUsed] = useState<[number, number]>([0, 0]);
141144

142145
const getToolsForAgent = useTools();
143-
const [hasToolResultFollowUp, setHasToolResultFollowUp] = useState(false);
144146

145147
const [agentConfigOpen, setAgentConfigOpen] = useState(false);
146148
const pickAgent = useAutoAgentSelect();
147149

150+
const [shouldRegenMessages, setShouldRegenMessages] = useState(false);
151+
148152
const normalizeAndApplyContext = useProcessMessages();
149153

150154
const { tools: atomicTools } = useAtomicMCPTools({
@@ -225,7 +229,12 @@ export const SimpleAIChat: React.FC<
225229
setUserSelectedContextItems(newContextItems);
226230
};
227231

228-
const sendMessage = async (isFollowUp = false) => {
232+
const sendMessage = async ({
233+
regenerate = false,
234+
}: {
235+
/** Whether to regenerate the response */
236+
regenerate?: boolean;
237+
} = {}) => {
229238
if (readonly) {
230239
toast.error('You do not have the permissions to edit this chat.');
231240

@@ -281,21 +290,28 @@ export const SimpleAIChat: React.FC<
281290
...userSelectedContextItems,
282291
];
283292

284-
if (!isFollowUp) {
293+
if (regenerate) {
294+
messagesToUse = messages;
295+
} else {
285296
const userMessage = prepareUserMessage(
286297
userInput,
287298
attachedFile,
288299
allContextItems,
289300
);
290301
messagesToUse = [...messages, userMessage];
291302
onNewMessage(userMessage);
292-
} else {
293-
messagesToUse = messages;
294303
}
295304

296305
// Filter message to only include non-error messages, error messages are only intended for the user.
297306
const filteredMessages = await normalizeAndApplyContext(messagesToUse);
298307

308+
console.log(
309+
'last message sent as context: "',
310+
filteredMessages.at(-1).content[0].text,
311+
'other mgss',
312+
filteredMessages,
313+
);
314+
299315
// Update messages with the user message first
300316
setExternalContextItems([]);
301317
setUserSelectedContextItems([]);
@@ -545,13 +561,6 @@ export const SimpleAIChat: React.FC<
545561
}
546562
};
547563

548-
useEffect(() => {
549-
if (hasToolResultFollowUp) {
550-
setHasToolResultFollowUp(false);
551-
sendMessage(true);
552-
}
553-
}, [hasToolResultFollowUp]);
554-
555564
// Combine both context item lists when needed
556565
const allContextItems = [
557566
...externalContextItems,
@@ -564,18 +573,37 @@ export const SimpleAIChat: React.FC<
564573
});
565574
};
566575

576+
const regenerateMessage = async (message: AIChatDisplayMessage) => {
577+
flushSync(async () => {
578+
// Removes the following messages
579+
await onRegenerateMessage(message);
580+
581+
setShouldRegenMessages(true);
582+
});
583+
};
584+
585+
useEffect(() => {
586+
if (shouldRegenMessages) {
587+
sendMessage({
588+
regenerate: true,
589+
});
590+
setShouldRegenMessages(false);
591+
}
592+
}, [shouldRegenMessages]);
593+
567594
return (
568595
<ChatWindow fullView={fullView}>
569596
{children}
570597
<ChatMessagesContainer
571598
enableAutoScroll={aiState !== AIState.Stopped}
572599
fullView={fullView}
573600
>
574-
{messages.filter(cleanMessages).map(message => (
601+
{messages.filter(cleanMessages).map((message, index) => (
575602
<AIChatMessage
576-
key={JSON.stringify(message)}
603+
key={`${JSON.stringify(message)}-${index}`}
577604
message={message}
578605
onDeleteMessage={onDeleteMessage}
606+
onRegenerateMessage={regenerateMessage}
579607
/>
580608
))}
581609
{ongoingMessage.text && (

browser/data-browser/src/components/AI/chatConversionUtils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { newContextItem } from './AISidebarContext';
1717
import {
1818
type AIAtomicResourceMessageContext,
1919
type AIChatDisplayMessage,
20+
AIChatErrorMessage,
2021
type AIMCPResourceMessageContext,
2122
type AIMessageContext,
2223
isAtomicResource,
@@ -157,7 +158,15 @@ export const messageResourcesToDisplayMessages = async (
157158

158159
for (const resource of resources) {
159160
if (resource.error) {
160-
throw resource.error;
161+
console.error(resource.error);
162+
messages.set(
163+
{
164+
role: 'error',
165+
content: resource.error.message,
166+
} satisfies AIChatErrorMessage,
167+
resource,
168+
);
169+
continue;
161170
}
162171

163172
const role = tagToRole(resource.props.role);

browser/data-browser/src/components/ErrorLook.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { styled, css } from 'styled-components';
44
import { FaExclamationTriangle } from 'react-icons/fa';
55

66
import type { JSX } from 'react';
7+
import { getMessageForErrorType } from '@tomic/react';
78

89
export const errorLookStyle = css`
910
color: ${props => props.theme.colors.alert};
@@ -25,7 +26,7 @@ export function ErrorBlock({ error, showTrace }: ErrorBlockProps): JSX.Element {
2526
<ErrorLookBig>
2627
<BiggerText>
2728
<FaExclamationTriangle />
28-
Something went wrong
29+
{getMessageForErrorType(error)}
2930
</BiggerText>
3031
<Pre>
3132
<code>{error.message}</code>

browser/data-browser/src/helpers/formatCurrency.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

browser/data-browser/src/views/AIChat/AIChatPage.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from 'react';
22
import { SimpleAIChat } from '../../components/AI/SimpleAIChat';
33
import {
4+
Ai,
45
ai,
56
useArray,
67
useCanWrite,
@@ -23,8 +24,11 @@ import {
2324
messageResourcesToDisplayMessages,
2425
} from '../../components/AI/chatConversionUtils';
2526
import { TagBar } from '../../components/Tag/TagBar';
27+
import { flushSync } from 'react-dom';
2628

27-
export const AIChatPage: React.FC<ResourcePageProps> = ({ resource }) => {
29+
export const AIChatPage: React.FC<ResourcePageProps<Ai.AiChat>> = ({
30+
resource,
31+
}) => {
2832
const store = useStore();
2933
const [messages, setMessages] = useState<AIChatDisplayMessage[]>([]);
3034
const [contextItems, setContextItems] = useState<AIMessageContext[]>([]);
@@ -87,6 +91,54 @@ export const AIChatPage: React.FC<ResourcePageProps> = ({ resource }) => {
8791
});
8892
};
8993

94+
const removeFollowingMessages = async (message: AIChatDisplayMessage) => {
95+
const nextMessages = messages.slice(messages.indexOf(message) + 1);
96+
97+
// We need to destroy the resources server side as well as in the internal state.
98+
// We also need to update the `messages` prop in the chat resource.
99+
const destroySubjects: string[] = [];
100+
101+
for (const m of nextMessages) {
102+
const r = messageToResourceMap.get(m);
103+
104+
if (r) {
105+
destroySubjects.push(r.subject);
106+
107+
try {
108+
await r.destroy();
109+
} catch (error) {
110+
console.error('Error removing message:', error);
111+
}
112+
} else {
113+
throw new Error('Resource not found for message:', m);
114+
}
115+
}
116+
117+
try {
118+
// Set chat resource on server with new message array
119+
await resource.set(
120+
ai.properties.messages,
121+
resource.props.messages?.filter(x => !destroySubjects.includes(x)),
122+
);
123+
await resource.save();
124+
// Set internal message state
125+
setMessages(prev => {
126+
const newMessages = prev.slice(0, prev.indexOf(message) + 1);
127+
128+
console.log(
129+
'setting messages',
130+
newMessages,
131+
'last message',
132+
newMessages.at(-1).content[0].text,
133+
);
134+
135+
return newMessages;
136+
});
137+
} catch (error) {
138+
console.error('Error removing messages:', error);
139+
}
140+
};
141+
90142
// On load create AIChatDisplayMessages from the resource's messages.
91143
useEffect(() => {
92144
messageResourcesToDisplayMessages(messageSubjects, store).then(map => {
@@ -110,6 +162,7 @@ export const AIChatPage: React.FC<ResourcePageProps> = ({ resource }) => {
110162
externalContextItems={contextItems}
111163
setExternalContextItems={setContextItems}
112164
onDeleteMessage={handleDeleteMessage}
165+
onRegenerateMessage={removeFollowingMessages}
113166
>
114167
<Column gap='0.5rem'>
115168
<Row>

0 commit comments

Comments
 (0)