Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhanced Code Context and Project Summary Features #1191

Merged
76 changes: 75 additions & 1 deletion app/components/chat/AssistantMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,38 @@ import { Markdown } from './Markdown';
import type { JSONValue } from 'ai';
import type { ProgressAnnotation } from '~/types/context';
import Popover from '~/components/ui/Popover';
import { workbenchStore } from '~/lib/stores/workbench';
import { WORK_DIR } from '~/utils/constants';

interface AssistantMessageProps {
content: string;
annotations?: JSONValue[];
}

function openArtifactInWorkbench(filePath: string) {
filePath = normalizedFilePath(filePath);

if (workbenchStore.currentView.get() !== 'code') {
workbenchStore.currentView.set('code');
}

workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`);
}

function normalizedFilePath(path: string) {
let normalizedPath = path;

if (normalizedPath.startsWith(WORK_DIR)) {
normalizedPath = path.replace(WORK_DIR, '');
}

if (normalizedPath.startsWith('/')) {
normalizedPath = normalizedPath.slice(1);
}

return normalizedPath;
}

export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
const filteredAnnotations = (annotations?.filter(
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
Expand All @@ -19,6 +45,18 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
) as ProgressAnnotation[];
progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value);

let chatSummary: string | undefined = undefined;

if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
}

let codeContext: string[] | undefined = undefined;

if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) {
codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files;
}

const usage: {
completionTokens: number;
promptTokens: number;
Expand All @@ -30,7 +68,43 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
<>
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
{progressAnnotation.length > 0 && (
<Popover trigger={<div className="i-ph:info" />}>{progressAnnotation[0].message}</Popover>
<Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
{chatSummary && (
<div>
<div className="summary max-h-96 flex flex-col">
<h2 className="border border-bolt-elements-borderColor rounded-md p4">Summary</h2>
<div style={{ zoom: 0.6 }} className="overflow-y-auto m4">
<Markdown>{chatSummary}</Markdown>
</div>
</div>
{codeContext && (
<div className="code-context flex flex-col p4 border border-bolt-elements-borderColor rounded-md">
<h2>Context</h2>
<div className="flex gap-4 mt-4 bolt" style={{ zoom: 0.6 }}>
{codeContext.map((x) => {
const normalized = normalizedFilePath(x);
return (
<>
<code
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"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openArtifactInWorkbench(normalized);
}}
>
{normalized}
</code>
</>
);
})}
</div>
</div>
)}
</div>
)}
<div className="context"></div>
</Popover>
)}
{usage && (
<div>
Expand Down
2 changes: 0 additions & 2 deletions app/components/chat/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
const components = useMemo(() => {
return {
div: ({ className, children, node, ...props }) => {
console.log(className, node);

if (className?.includes('__boltArtifact__')) {
const messageId = node?.properties.dataMessageId as string;

Expand Down
15 changes: 12 additions & 3 deletions app/components/ui/Popover.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import * as Popover from '@radix-ui/react-popover';
import type { PropsWithChildren, ReactNode } from 'react';

export default ({ children, trigger }: PropsWithChildren<{ trigger: ReactNode }>) => (
export default ({
children,
trigger,
side,
align,
}: PropsWithChildren<{
trigger: ReactNode;
side: 'top' | 'right' | 'bottom' | 'left' | undefined;
align: 'center' | 'start' | 'end' | undefined;
}>) => (
<Popover.Root>
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
<Popover.Anchor />
<Popover.Portal>
<Popover.Content
sideOffset={10}
side="top"
align="center"
side={side}
align={align}
className="bg-bolt-elements-background-depth-2 text-bolt-elements-item-contentAccent p-2 rounded-md shadow-xl z-workbench"
>
{children}
Expand Down
76 changes: 67 additions & 9 deletions app/lib/.server/llm/create-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function createSummary(props: {
contextOptimization?: boolean;
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
}) {
const { messages, env: serverEnv, apiKeys, providerSettings, contextOptimization, onFinish } = props;
const { messages, env: serverEnv, apiKeys, providerSettings, onFinish } = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
const processedMessages = messages.map((message) => {
Expand All @@ -29,9 +29,8 @@ export async function createSummary(props: {
} else if (message.role == 'assistant') {
let content = message.content;

if (contextOptimization) {
content = simplifyBoltActions(content);
}
content = simplifyBoltActions(content);
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');

return { ...message, content };
}
Expand Down Expand Up @@ -92,6 +91,8 @@ ${summary.summary}`;
}
}

logger.debug('Sliced Messages:', slicedMessages.length);

const extractTextContent = (message: Message) =>
Array.isArray(message.content)
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
Expand All @@ -100,25 +101,82 @@ ${summary.summary}`;
// select files from the list of code file from the project that might be useful for the current request from the user
const resp = await generateText({
system: `
You are a software engineer. You are working on a project. tou need to summarize the work till now and provide a summary of the chat till now.
You are a software engineer. You are working on a project. you need to summarize the work till now and provide a summary of the chat till now.

Please only use the following format to generate the summary:
---
# Project Overview
- **Project**: {project_name} - {brief_description}
- **Current Phase**: {phase}
- **Tech Stack**: {languages}, {frameworks}, {key_dependencies}
- **Environment**: {critical_env_details}

# Conversation Context
- **Last Topic**: {main_discussion_point}
- **Key Decisions**: {important_decisions_made}
- **User Context**:
- Technical Level: {expertise_level}
- Preferences: {coding_style_preferences}
- Communication: {preferred_explanation_style}

# Implementation Status
## Current State
- **Active Feature**: {feature_in_development}
- **Progress**: {what_works_and_what_doesn't}
- **Blockers**: {current_challenges}

## Code Evolution
- **Recent Changes**: {latest_modifications}
- **Working Patterns**: {successful_approaches}
- **Failed Approaches**: {attempted_solutions_that_failed}

# Requirements
- **Implemented**: {completed_features}
- **In Progress**: {current_focus}
- **Pending**: {upcoming_features}
- **Technical Constraints**: {critical_constraints}

# Critical Memory
- **Must Preserve**: {crucial_technical_context}
- **User Requirements**: {specific_user_needs}
- **Known Issues**: {documented_problems}

# Next Actions
- **Immediate**: {next_steps}
- **Open Questions**: {unresolved_issues}

---
Note:
4. Keep entries concise and focused on information needed for continuity


${summaryText}
---

RULES:
* Only provide the summary of the chat till now.
* Only provide the whole summary of the chat till now.
* Do not provide any new information.
* DO not need to think too much just start writing imidiately
* do not write any thing other that the summary with with the provided structure
`,
prompt: `
please provide a summary of the chat till now.
below is the latest chat:

Here is the previous summary of the chat:
<old_summary>
${summaryText}
</old_summary>

Below is the chat after that:
---
<new_chats>
${slicedMessages
.map((x) => {
return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
})
.join('\n')}
</new_chats>
---

Please provide a summary of the chat till now including the hitorical summary of the chat.
`,
model: provider.getModelInstance({
model: currentModel,
Expand Down
8 changes: 4 additions & 4 deletions app/lib/.server/llm/select-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function selectContext(props: {
summary: string;
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
}) {
const { messages, env: serverEnv, apiKeys, files, providerSettings, contextOptimization, summary, onFinish } = props;
const { messages, env: serverEnv, apiKeys, files, providerSettings, summary, onFinish } = props;
let currentModel = DEFAULT_MODEL;
let currentProvider = DEFAULT_PROVIDER.name;
const processedMessages = messages.map((message) => {
Expand All @@ -36,9 +36,9 @@ export async function selectContext(props: {
} else if (message.role == 'assistant') {
let content = message.content;

if (contextOptimization) {
content = simplifyBoltActions(content);
}
content = simplifyBoltActions(content);

content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');

return { ...message, content };
}
Expand Down
20 changes: 11 additions & 9 deletions app/lib/.server/llm/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PromptLibrary } from '~/lib/common/prompt-library';
import { allowedHTMLElements } from '~/utils/markdown';
import { LLMManager } from '~/lib/modules/llm/manager';
import { createScopedLogger } from '~/utils/logger';
import { createFilesContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
import { createFilesContext, extractPropertiesFromMessage } from './utils';
import { getFilePaths } from './select-context';

export type Messages = Message[];
Expand All @@ -27,6 +27,7 @@ export async function streamText(props: {
contextOptimization?: boolean;
contextFiles?: FileMap;
summary?: string;
messageSliceId?: number;
}) {
const {
messages,
Expand All @@ -51,10 +52,7 @@ export async function streamText(props: {
return { ...message, content };
} else if (message.role == 'assistant') {
let content = message.content;

if (contextOptimization) {
content = simplifyBoltActions(content);
}
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');

return { ...message, content };
}
Expand Down Expand Up @@ -110,7 +108,7 @@ Below are all the files present in the project:
${filePaths.join('\n')}
---

Below is the context loaded into context buffer for you to have knowledge of and might need changes to fullfill current user request.
Below is the artifact containing the context loaded into context buffer for you to have knowledge of and might need changes to fullfill current user request.
CONTEXT BUFFER:
---
${codeContext}
Expand All @@ -126,10 +124,14 @@ ${props.summary}
---
`;

const lastMessage = processedMessages.pop();
if (props.messageSliceId) {
processedMessages = processedMessages.slice(props.messageSliceId);
} else {
const lastMessage = processedMessages.pop();

if (lastMessage) {
processedMessages = [lastMessage];
if (lastMessage) {
processedMessages = [lastMessage];
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/lib/.server/llm/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ export function createFilesContext(files: FileMap, useRelativePath?: boolean) {
filePath = path.replace('/home/project/', '');
}

return `<file path="${filePath}">\n${codeWithLinesNumbers}\n</file>`;
return `<boltAction type="file" filePath="${filePath}">${codeWithLinesNumbers}</boltAction>`;
});

return `<codebase>${fileContexts.join('\n\n')}\n\n</codebase>`;
return `<boltArtifact id="code-content" title="Code Content" >\n${fileContexts.join('\n')}\n</boltArtifact>`;
}

export function extractCurrentContext(messages: Message[]) {
Expand Down
7 changes: 6 additions & 1 deletion app/lib/modules/llm/providers/groq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ export default class GroqProvider extends BaseProvider {
{ name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 },
{ name: 'deepseek-r1-distill-llama-70b', label: 'Deepseek R1 Distill Llama 70b (Groq)', provider: 'Groq', maxTokenAllowed: 131072 },
{
name: 'deepseek-r1-distill-llama-70b',
label: 'Deepseek R1 Distill Llama 70b (Groq)',
provider: 'Groq',
maxTokenAllowed: 131072,
},
];

getModelInstance(options: {
Expand Down
Loading
Loading