Skip to content

Commit 7016111

Browse files
authored
feat: enhanced Code Context and Project Summary Features (#1191)
* fix: docker prod env variable fix * lint and typecheck * removed hardcoded tag * better summary generation * improved summary generation for context optimization * remove think tags from the generation
1 parent a199295 commit 7016111

File tree

14 files changed

+413
-83
lines changed

14 files changed

+413
-83
lines changed

app/components/chat/AssistantMessage.tsx

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,55 @@
11
import { memo } from 'react';
22
import { Markdown } from './Markdown';
33
import type { JSONValue } from 'ai';
4-
import type { ProgressAnnotation } from '~/types/context';
54
import Popover from '~/components/ui/Popover';
5+
import { workbenchStore } from '~/lib/stores/workbench';
6+
import { WORK_DIR } from '~/utils/constants';
67

78
interface AssistantMessageProps {
89
content: string;
910
annotations?: JSONValue[];
1011
}
1112

13+
function openArtifactInWorkbench(filePath: string) {
14+
filePath = normalizedFilePath(filePath);
15+
16+
if (workbenchStore.currentView.get() !== 'code') {
17+
workbenchStore.currentView.set('code');
18+
}
19+
20+
workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
21+
}
22+
23+
function normalizedFilePath(path: string) {
24+
let normalizedPath = path;
25+
26+
if (normalizedPath.startsWith(WORK_DIR)) {
27+
normalizedPath = path.replace(WORK_DIR, '');
28+
}
29+
30+
if (normalizedPath.startsWith('/')) {
31+
normalizedPath = normalizedPath.slice(1);
32+
}
33+
34+
return normalizedPath;
35+
}
36+
1237
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
1338
const filteredAnnotations = (annotations?.filter(
1439
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
1540
) || []) as { type: string; value: any } & { [key: string]: any }[];
1641

17-
let progressAnnotation: ProgressAnnotation[] = filteredAnnotations.filter(
18-
(annotation) => annotation.type === 'progress',
19-
) as ProgressAnnotation[];
20-
progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value);
42+
let chatSummary: string | undefined = undefined;
43+
44+
if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
45+
chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
46+
}
47+
48+
let codeContext: string[] | undefined = undefined;
49+
50+
if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) {
51+
codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files;
52+
}
2153

2254
const usage: {
2355
completionTokens: number;
@@ -29,8 +61,44 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
2961
<div className="overflow-hidden w-full">
3062
<>
3163
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
32-
{progressAnnotation.length > 0 && (
33-
<Popover trigger={<div className="i-ph:info" />}>{progressAnnotation[0].message}</Popover>
64+
{(codeContext || chatSummary) && (
65+
<Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
66+
{chatSummary && (
67+
<div className="max-w-chat">
68+
<div className="summary max-h-96 flex flex-col">
69+
<h2 className="border border-bolt-elements-borderColor rounded-md p4">Summary</h2>
70+
<div style={{ zoom: 0.7 }} className="overflow-y-auto m4">
71+
<Markdown>{chatSummary}</Markdown>
72+
</div>
73+
</div>
74+
{codeContext && (
75+
<div className="code-context flex flex-col p4 border border-bolt-elements-borderColor rounded-md">
76+
<h2>Context</h2>
77+
<div className="flex gap-4 mt-4 bolt" style={{ zoom: 0.6 }}>
78+
{codeContext.map((x) => {
79+
const normalized = normalizedFilePath(x);
80+
return (
81+
<>
82+
<code
83+
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
84+
onClick={(e) => {
85+
e.preventDefault();
86+
e.stopPropagation();
87+
openArtifactInWorkbench(normalized);
88+
}}
89+
>
90+
{normalized}
91+
</code>
92+
</>
93+
);
94+
})}
95+
</div>
96+
</div>
97+
)}
98+
</div>
99+
)}
100+
<div className="context"></div>
101+
</Popover>
34102
)}
35103
{usage && (
36104
<div>

app/components/chat/BaseChat.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @ts-nocheck
33
* Preventing TS checks with files presented in the video for a better presentation.
44
*/
5-
import type { Message } from 'ai';
5+
import type { JSONValue, Message } from 'ai';
66
import React, { type RefCallback, useEffect, useState } from 'react';
77
import { ClientOnly } from 'remix-utils/client-only';
88
import { Menu } from '~/components/sidebar/Menu.client';
@@ -32,6 +32,8 @@ import StarterTemplates from './StarterTemplates';
3232
import type { ActionAlert } from '~/types/actions';
3333
import ChatAlert from './ChatAlert';
3434
import type { ModelInfo } from '~/lib/modules/llm/types';
35+
import ProgressCompilation from './ProgressCompilation';
36+
import type { ProgressAnnotation } from '~/types/context';
3537

3638
const TEXTAREA_MIN_HEIGHT = 76;
3739

@@ -64,6 +66,7 @@ interface BaseChatProps {
6466
setImageDataList?: (dataList: string[]) => void;
6567
actionAlert?: ActionAlert;
6668
clearAlert?: () => void;
69+
data?: JSONValue[] | undefined;
6770
}
6871

6972
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -97,6 +100,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
97100
messages,
98101
actionAlert,
99102
clearAlert,
103+
data,
100104
},
101105
ref,
102106
) => {
@@ -108,7 +112,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
108112
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109113
const [transcript, setTranscript] = useState('');
110114
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
111-
115+
const [progressAnnotations, setProgressAnnotations] = useState<ProgressAnnotation[]>([]);
116+
useEffect(() => {
117+
if (data) {
118+
const progressList = data.filter(
119+
(x) => typeof x === 'object' && (x as any).type === 'progress',
120+
) as ProgressAnnotation[];
121+
setProgressAnnotations(progressList);
122+
}
123+
}, [data]);
112124
useEffect(() => {
113125
console.log(transcript);
114126
}, [transcript]);
@@ -307,6 +319,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
307319
className={classNames('pt-6 px-2 sm:px-6', {
308320
'h-full flex flex-col': chatStarted,
309321
})}
322+
ref={scrollRef}
310323
>
311324
<ClientOnly>
312325
{() => {
@@ -337,6 +350,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
337350
/>
338351
)}
339352
</div>
353+
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
340354
<div
341355
className={classNames(
342356
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',

app/components/chat/Chat.client.tsx

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -137,36 +137,49 @@ export const ChatImpl = memo(
137137

138138
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
139139

140-
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
141-
useChat({
142-
api: '/api/chat',
143-
body: {
144-
apiKeys,
145-
files,
146-
promptId,
147-
contextOptimization: contextOptimizationEnabled,
148-
},
149-
sendExtraMessageFields: true,
150-
onError: (e) => {
151-
logger.error('Request failed\n\n', e, error);
152-
toast.error(
153-
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
154-
);
155-
},
156-
onFinish: (message, response) => {
157-
const usage = response.usage;
158-
159-
if (usage) {
160-
console.log('Token usage:', usage);
161-
162-
// You can now use the usage data as needed
163-
}
140+
const {
141+
messages,
142+
isLoading,
143+
input,
144+
handleInputChange,
145+
setInput,
146+
stop,
147+
append,
148+
setMessages,
149+
reload,
150+
error,
151+
data: chatData,
152+
setData,
153+
} = useChat({
154+
api: '/api/chat',
155+
body: {
156+
apiKeys,
157+
files,
158+
promptId,
159+
contextOptimization: contextOptimizationEnabled,
160+
},
161+
sendExtraMessageFields: true,
162+
onError: (e) => {
163+
logger.error('Request failed\n\n', e, error);
164+
toast.error(
165+
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
166+
);
167+
},
168+
onFinish: (message, response) => {
169+
const usage = response.usage;
170+
setData(undefined);
171+
172+
if (usage) {
173+
console.log('Token usage:', usage);
174+
175+
// You can now use the usage data as needed
176+
}
164177

165-
logger.debug('Finished streaming');
166-
},
167-
initialMessages,
168-
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
169-
});
178+
logger.debug('Finished streaming');
179+
},
180+
initialMessages,
181+
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
182+
});
170183
useEffect(() => {
171184
const prompt = searchParams.get('prompt');
172185

@@ -535,6 +548,7 @@ export const ChatImpl = memo(
535548
setImageDataList={setImageDataList}
536549
actionAlert={actionAlert}
537550
clearAlert={() => workbenchStore.clearAlert()}
551+
data={chatData}
538552
/>
539553
);
540554
},

app/components/chat/Markdown.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
2323
const components = useMemo(() => {
2424
return {
2525
div: ({ className, children, node, ...props }) => {
26-
console.log(className, node);
27-
2826
if (className?.includes('__boltArtifact__')) {
2927
const messageId = node?.properties.dataMessageId as string;
3028

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { AnimatePresence, motion } from 'framer-motion';
2+
import React, { useState } from 'react';
3+
import type { ProgressAnnotation } from '~/types/context';
4+
import { classNames } from '~/utils/classNames';
5+
import { cubicEasingFn } from '~/utils/easings';
6+
7+
export default function ProgressCompilation({ data }: { data?: ProgressAnnotation[] }) {
8+
const [progressList, setProgressList] = React.useState<ProgressAnnotation[]>([]);
9+
const [expanded, setExpanded] = useState(false);
10+
React.useEffect(() => {
11+
if (!data || data.length == 0) {
12+
setProgressList([]);
13+
return;
14+
}
15+
16+
const progressMap = new Map<string, ProgressAnnotation>();
17+
data.forEach((x) => {
18+
const existingProgress = progressMap.get(x.label);
19+
20+
if (existingProgress && existingProgress.status === 'complete') {
21+
return;
22+
}
23+
24+
progressMap.set(x.label, x);
25+
});
26+
27+
const newData = Array.from(progressMap.values());
28+
newData.sort((a, b) => a.order - b.order);
29+
setProgressList(newData);
30+
}, [data]);
31+
32+
if (progressList.length === 0) {
33+
return <></>;
34+
}
35+
36+
return (
37+
<AnimatePresence>
38+
<div
39+
className={classNames(
40+
'bg-bolt-elements-background-depth-2',
41+
'border border-bolt-elements-borderColor',
42+
'shadow-lg rounded-lg relative w-full max-w-chat mx-auto z-prompt',
43+
'p-1',
44+
)}
45+
style={{ transform: 'translateY(1rem)' }}
46+
>
47+
<div
48+
className={classNames(
49+
'bg-bolt-elements-item-backgroundAccent',
50+
'p-1 rounded-lg text-bolt-elements-item-contentAccent',
51+
'flex ',
52+
)}
53+
>
54+
<div className="flex-1">
55+
<AnimatePresence>
56+
{expanded ? (
57+
<motion.div
58+
className="actions"
59+
initial={{ height: 0 }}
60+
animate={{ height: 'auto' }}
61+
exit={{ height: '0px' }}
62+
transition={{ duration: 0.15 }}
63+
>
64+
{progressList.map((x, i) => {
65+
return <ProgressItem key={i} progress={x} />;
66+
})}
67+
</motion.div>
68+
) : (
69+
<ProgressItem progress={progressList.slice(-1)[0]} />
70+
)}
71+
</AnimatePresence>
72+
</div>
73+
<motion.button
74+
initial={{ width: 0 }}
75+
animate={{ width: 'auto' }}
76+
exit={{ width: 0 }}
77+
transition={{ duration: 0.15, ease: cubicEasingFn }}
78+
className=" p-1 rounded-lg bg-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-artifacts-backgroundHover"
79+
onClick={() => setExpanded((v) => !v)}
80+
>
81+
<div className={expanded ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
82+
</motion.button>
83+
</div>
84+
</div>
85+
</AnimatePresence>
86+
);
87+
}
88+
89+
const ProgressItem = ({ progress }: { progress: ProgressAnnotation }) => {
90+
return (
91+
<motion.div
92+
className={classNames('flex text-sm gap-3')}
93+
initial={{ opacity: 0 }}
94+
animate={{ opacity: 1 }}
95+
exit={{ opacity: 0 }}
96+
transition={{ duration: 0.15 }}
97+
>
98+
<div className="flex items-center gap-1.5 ">
99+
<div>
100+
{progress.status === 'in-progress' ? (
101+
<div className="i-svg-spinners:90-ring-with-bg"></div>
102+
) : progress.status === 'complete' ? (
103+
<div className="i-ph:check"></div>
104+
) : null}
105+
</div>
106+
{/* {x.label} */}
107+
</div>
108+
{progress.message}
109+
</motion.div>
110+
);
111+
};

0 commit comments

Comments
 (0)