mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-02 03:14:11 +00:00
Merge branch 'stackblitz-labs:main' into FEAT_BoltDYI_NEW_SETTINGS_UI_V2
This commit is contained in:
commit
547cde78c0
@ -1,23 +1,55 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import type { JSONValue } from 'ai';
|
import type { JSONValue } from 'ai';
|
||||||
import type { ProgressAnnotation } from '~/types/context';
|
|
||||||
import Popover from '~/components/ui/Popover';
|
import Popover from '~/components/ui/Popover';
|
||||||
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
|
|
||||||
interface AssistantMessageProps {
|
interface AssistantMessageProps {
|
||||||
content: string;
|
content: string;
|
||||||
annotations?: JSONValue[];
|
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) => {
|
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
|
||||||
const filteredAnnotations = (annotations?.filter(
|
const filteredAnnotations = (annotations?.filter(
|
||||||
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||||
) || []) as { type: string; value: any } & { [key: string]: any }[];
|
) || []) as { type: string; value: any } & { [key: string]: any }[];
|
||||||
|
|
||||||
let progressAnnotation: ProgressAnnotation[] = filteredAnnotations.filter(
|
let chatSummary: string | undefined = undefined;
|
||||||
(annotation) => annotation.type === 'progress',
|
|
||||||
) as ProgressAnnotation[];
|
if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
|
||||||
progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value);
|
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: {
|
const usage: {
|
||||||
completionTokens: number;
|
completionTokens: number;
|
||||||
@ -29,8 +61,44 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
|
|||||||
<div className="overflow-hidden w-full">
|
<div className="overflow-hidden w-full">
|
||||||
<>
|
<>
|
||||||
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
|
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
|
||||||
{progressAnnotation.length > 0 && (
|
{(codeContext || chatSummary) && (
|
||||||
<Popover trigger={<div className="i-ph:info" />}>{progressAnnotation[0].message}</Popover>
|
<Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
|
||||||
|
{chatSummary && (
|
||||||
|
<div className="max-w-chat">
|
||||||
|
<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.7 }} 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 && (
|
{usage && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* @ts-nocheck
|
* @ts-nocheck
|
||||||
* Preventing TS checks with files presented in the video for a better presentation.
|
* Preventing TS checks with files presented in the video for a better presentation.
|
||||||
*/
|
*/
|
||||||
import type { Message } from 'ai';
|
import type { JSONValue, Message } from 'ai';
|
||||||
import React, { type RefCallback, useEffect, useState } from 'react';
|
import React, { type RefCallback, useEffect, useState } from 'react';
|
||||||
import { ClientOnly } from 'remix-utils/client-only';
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { Menu } from '~/components/sidebar/Menu.client';
|
import { Menu } from '~/components/sidebar/Menu.client';
|
||||||
@ -32,6 +32,8 @@ import StarterTemplates from './StarterTemplates';
|
|||||||
import type { ActionAlert } from '~/types/actions';
|
import type { ActionAlert } from '~/types/actions';
|
||||||
import ChatAlert from './ChatAlert';
|
import ChatAlert from './ChatAlert';
|
||||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||||
|
import ProgressCompilation from './ProgressCompilation';
|
||||||
|
import type { ProgressAnnotation } from '~/types/context';
|
||||||
|
|
||||||
const TEXTAREA_MIN_HEIGHT = 76;
|
const TEXTAREA_MIN_HEIGHT = 76;
|
||||||
|
|
||||||
@ -64,6 +66,7 @@ interface BaseChatProps {
|
|||||||
setImageDataList?: (dataList: string[]) => void;
|
setImageDataList?: (dataList: string[]) => void;
|
||||||
actionAlert?: ActionAlert;
|
actionAlert?: ActionAlert;
|
||||||
clearAlert?: () => void;
|
clearAlert?: () => void;
|
||||||
|
data?: JSONValue[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||||
@ -97,6 +100,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
messages,
|
messages,
|
||||||
actionAlert,
|
actionAlert,
|
||||||
clearAlert,
|
clearAlert,
|
||||||
|
data,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -108,7 +112,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
||||||
const [transcript, setTranscript] = useState('');
|
const [transcript, setTranscript] = useState('');
|
||||||
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
|
const [isModelLoading, setIsModelLoading] = useState<string | undefined>('all');
|
||||||
|
const [progressAnnotations, setProgressAnnotations] = useState<ProgressAnnotation[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
const progressList = data.filter(
|
||||||
|
(x) => typeof x === 'object' && (x as any).type === 'progress',
|
||||||
|
) as ProgressAnnotation[];
|
||||||
|
setProgressAnnotations(progressList);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(transcript);
|
console.log(transcript);
|
||||||
}, [transcript]);
|
}, [transcript]);
|
||||||
@ -307,6 +319,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
className={classNames('pt-6 px-2 sm:px-6', {
|
className={classNames('pt-6 px-2 sm:px-6', {
|
||||||
'h-full flex flex-col': chatStarted,
|
'h-full flex flex-col': chatStarted,
|
||||||
})}
|
})}
|
||||||
|
ref={scrollRef}
|
||||||
>
|
>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
{() => {
|
{() => {
|
||||||
@ -337,6 +350,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'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',
|
'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',
|
||||||
|
@ -137,36 +137,49 @@ export const ChatImpl = memo(
|
|||||||
|
|
||||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const { messages, isLoading, input, handleInputChange, setInput, stop, append, setMessages, reload, error } =
|
const {
|
||||||
useChat({
|
messages,
|
||||||
api: '/api/chat',
|
isLoading,
|
||||||
body: {
|
input,
|
||||||
apiKeys,
|
handleInputChange,
|
||||||
files,
|
setInput,
|
||||||
promptId,
|
stop,
|
||||||
contextOptimization: contextOptimizationEnabled,
|
append,
|
||||||
},
|
setMessages,
|
||||||
sendExtraMessageFields: true,
|
reload,
|
||||||
onError: (e) => {
|
error,
|
||||||
logger.error('Request failed\n\n', e, error);
|
data: chatData,
|
||||||
toast.error(
|
setData,
|
||||||
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
} = useChat({
|
||||||
);
|
api: '/api/chat',
|
||||||
},
|
body: {
|
||||||
onFinish: (message, response) => {
|
apiKeys,
|
||||||
const usage = response.usage;
|
files,
|
||||||
|
promptId,
|
||||||
|
contextOptimization: contextOptimizationEnabled,
|
||||||
|
},
|
||||||
|
sendExtraMessageFields: true,
|
||||||
|
onError: (e) => {
|
||||||
|
logger.error('Request failed\n\n', e, error);
|
||||||
|
toast.error(
|
||||||
|
'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onFinish: (message, response) => {
|
||||||
|
const usage = response.usage;
|
||||||
|
setData(undefined);
|
||||||
|
|
||||||
if (usage) {
|
if (usage) {
|
||||||
console.log('Token usage:', usage);
|
console.log('Token usage:', usage);
|
||||||
|
|
||||||
// You can now use the usage data as needed
|
// You can now use the usage data as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Finished streaming');
|
logger.debug('Finished streaming');
|
||||||
},
|
},
|
||||||
initialMessages,
|
initialMessages,
|
||||||
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
|
initialInput: Cookies.get(PROMPT_COOKIE_KEY) || '',
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prompt = searchParams.get('prompt');
|
const prompt = searchParams.get('prompt');
|
||||||
|
|
||||||
@ -491,6 +504,7 @@ export const ChatImpl = memo(
|
|||||||
setImageDataList={setImageDataList}
|
setImageDataList={setImageDataList}
|
||||||
actionAlert={actionAlert}
|
actionAlert={actionAlert}
|
||||||
clearAlert={() => workbenchStore.clearAlert()}
|
clearAlert={() => workbenchStore.clearAlert()}
|
||||||
|
data={chatData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -23,8 +23,6 @@ export const Markdown = memo(({ children, html = false, limitedMarkdown = false
|
|||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
div: ({ className, children, node, ...props }) => {
|
div: ({ className, children, node, ...props }) => {
|
||||||
console.log(className, node);
|
|
||||||
|
|
||||||
if (className?.includes('__boltArtifact__')) {
|
if (className?.includes('__boltArtifact__')) {
|
||||||
const messageId = node?.properties.dataMessageId as string;
|
const messageId = node?.properties.dataMessageId as string;
|
||||||
|
|
||||||
|
111
app/components/chat/ProgressCompilation.tsx
Normal file
111
app/components/chat/ProgressCompilation.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import type { ProgressAnnotation } from '~/types/context';
|
||||||
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
|
|
||||||
|
export default function ProgressCompilation({ data }: { data?: ProgressAnnotation[] }) {
|
||||||
|
const [progressList, setProgressList] = React.useState<ProgressAnnotation[]>([]);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!data || data.length == 0) {
|
||||||
|
setProgressList([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressMap = new Map<string, ProgressAnnotation>();
|
||||||
|
data.forEach((x) => {
|
||||||
|
const existingProgress = progressMap.get(x.label);
|
||||||
|
|
||||||
|
if (existingProgress && existingProgress.status === 'complete') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressMap.set(x.label, x);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData = Array.from(progressMap.values());
|
||||||
|
newData.sort((a, b) => a.order - b.order);
|
||||||
|
setProgressList(newData);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (progressList.length === 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'bg-bolt-elements-background-depth-2',
|
||||||
|
'border border-bolt-elements-borderColor',
|
||||||
|
'shadow-lg rounded-lg relative w-full max-w-chat mx-auto z-prompt',
|
||||||
|
'p-1',
|
||||||
|
)}
|
||||||
|
style={{ transform: 'translateY(1rem)' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'bg-bolt-elements-item-backgroundAccent',
|
||||||
|
'p-1 rounded-lg text-bolt-elements-item-contentAccent',
|
||||||
|
'flex ',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<AnimatePresence>
|
||||||
|
{expanded ? (
|
||||||
|
<motion.div
|
||||||
|
className="actions"
|
||||||
|
initial={{ height: 0 }}
|
||||||
|
animate={{ height: 'auto' }}
|
||||||
|
exit={{ height: '0px' }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
{progressList.map((x, i) => {
|
||||||
|
return <ProgressItem key={i} progress={x} />;
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<ProgressItem progress={progressList.slice(-1)[0]} />
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: 'auto' }}
|
||||||
|
exit={{ width: 0 }}
|
||||||
|
transition={{ duration: 0.15, ease: cubicEasingFn }}
|
||||||
|
className=" p-1 rounded-lg bg-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-artifacts-backgroundHover"
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
>
|
||||||
|
<div className={expanded ? 'i-ph:caret-up-bold' : 'i-ph:caret-down-bold'}></div>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressItem = ({ progress }: { progress: ProgressAnnotation }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={classNames('flex text-sm gap-3')}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 ">
|
||||||
|
<div>
|
||||||
|
{progress.status === 'in-progress' ? (
|
||||||
|
<div className="i-svg-spinners:90-ring-with-bg"></div>
|
||||||
|
) : progress.status === 'complete' ? (
|
||||||
|
<div className="i-ph:check"></div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{/* {x.label} */}
|
||||||
|
</div>
|
||||||
|
{progress.message}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,15 +1,24 @@
|
|||||||
import * as Popover from '@radix-ui/react-popover';
|
import * as Popover from '@radix-ui/react-popover';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
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.Root>
|
||||||
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
|
<Popover.Trigger asChild>{trigger}</Popover.Trigger>
|
||||||
<Popover.Anchor />
|
<Popover.Anchor />
|
||||||
<Popover.Portal>
|
<Popover.Portal>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
side="top"
|
side={side}
|
||||||
align="center"
|
align={align}
|
||||||
className="bg-bolt-elements-background-depth-2 text-bolt-elements-item-contentAccent p-2 rounded-md shadow-xl z-workbench"
|
className="bg-bolt-elements-background-depth-2 text-bolt-elements-item-contentAccent p-2 rounded-md shadow-xl z-workbench"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -16,7 +16,7 @@ export async function createSummary(props: {
|
|||||||
contextOptimization?: boolean;
|
contextOptimization?: boolean;
|
||||||
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
|
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 currentModel = DEFAULT_MODEL;
|
||||||
let currentProvider = DEFAULT_PROVIDER.name;
|
let currentProvider = DEFAULT_PROVIDER.name;
|
||||||
const processedMessages = messages.map((message) => {
|
const processedMessages = messages.map((message) => {
|
||||||
@ -29,9 +29,9 @@ export async function createSummary(props: {
|
|||||||
} else if (message.role == 'assistant') {
|
} else if (message.role == 'assistant') {
|
||||||
let content = message.content;
|
let content = message.content;
|
||||||
|
|
||||||
if (contextOptimization) {
|
content = simplifyBoltActions(content);
|
||||||
content = simplifyBoltActions(content);
|
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');
|
||||||
}
|
content = content.replace(/<think>.*?<\/think>/s, '');
|
||||||
|
|
||||||
return { ...message, content };
|
return { ...message, content };
|
||||||
}
|
}
|
||||||
@ -92,6 +92,8 @@ ${summary.summary}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug('Sliced Messages:', slicedMessages.length);
|
||||||
|
|
||||||
const extractTextContent = (message: Message) =>
|
const extractTextContent = (message: Message) =>
|
||||||
Array.isArray(message.content)
|
Array.isArray(message.content)
|
||||||
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
|
? (message.content.find((item) => item.type === 'text')?.text as string) || ''
|
||||||
@ -100,25 +102,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
|
// 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({
|
const resp = await generateText({
|
||||||
system: `
|
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.
|
||||||
|
|
||||||
${summaryText}
|
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}
|
||||||
|
|
||||||
RULES:
|
# Conversation Context
|
||||||
* Only provide the summary of the chat till now.
|
- **Last Topic**: {main_discussion_point}
|
||||||
* Do not provide any new information.
|
- **Key Decisions**: {important_decisions_made}
|
||||||
`,
|
- **User Context**:
|
||||||
prompt: `
|
- Technical Level: {expertise_level}
|
||||||
please provide a summary of the chat till now.
|
- Preferences: {coding_style_preferences}
|
||||||
below is the latest chat:
|
- 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
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
* 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: `
|
||||||
|
|
||||||
|
Here is the previous summary of the chat:
|
||||||
|
<old_summary>
|
||||||
|
${summaryText}
|
||||||
|
</old_summary>
|
||||||
|
|
||||||
|
Below is the chat after that:
|
||||||
|
---
|
||||||
|
<new_chats>
|
||||||
${slicedMessages
|
${slicedMessages
|
||||||
.map((x) => {
|
.map((x) => {
|
||||||
return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
|
return `---\n[${x.role}] ${extractTextContent(x)}\n---`;
|
||||||
})
|
})
|
||||||
.join('\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: provider.getModelInstance({
|
||||||
model: currentModel,
|
model: currentModel,
|
||||||
|
@ -23,7 +23,7 @@ export async function selectContext(props: {
|
|||||||
summary: string;
|
summary: string;
|
||||||
onFinish?: (resp: GenerateTextResult<Record<string, CoreTool<any, any>>, never>) => void;
|
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 currentModel = DEFAULT_MODEL;
|
||||||
let currentProvider = DEFAULT_PROVIDER.name;
|
let currentProvider = DEFAULT_PROVIDER.name;
|
||||||
const processedMessages = messages.map((message) => {
|
const processedMessages = messages.map((message) => {
|
||||||
@ -36,9 +36,10 @@ export async function selectContext(props: {
|
|||||||
} else if (message.role == 'assistant') {
|
} else if (message.role == 'assistant') {
|
||||||
let content = message.content;
|
let content = message.content;
|
||||||
|
|
||||||
if (contextOptimization) {
|
content = simplifyBoltActions(content);
|
||||||
content = simplifyBoltActions(content);
|
|
||||||
}
|
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');
|
||||||
|
content = content.replace(/<think>.*?<\/think>/s, '');
|
||||||
|
|
||||||
return { ...message, content };
|
return { ...message, content };
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { PromptLibrary } from '~/lib/common/prompt-library';
|
|||||||
import { allowedHTMLElements } from '~/utils/markdown';
|
import { allowedHTMLElements } from '~/utils/markdown';
|
||||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
import { createFilesContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils';
|
import { createFilesContext, extractPropertiesFromMessage } from './utils';
|
||||||
import { getFilePaths } from './select-context';
|
import { getFilePaths } from './select-context';
|
||||||
|
|
||||||
export type Messages = Message[];
|
export type Messages = Message[];
|
||||||
@ -27,6 +27,7 @@ export async function streamText(props: {
|
|||||||
contextOptimization?: boolean;
|
contextOptimization?: boolean;
|
||||||
contextFiles?: FileMap;
|
contextFiles?: FileMap;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
messageSliceId?: number;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@ -51,10 +52,8 @@ export async function streamText(props: {
|
|||||||
return { ...message, content };
|
return { ...message, content };
|
||||||
} else if (message.role == 'assistant') {
|
} else if (message.role == 'assistant') {
|
||||||
let content = message.content;
|
let content = message.content;
|
||||||
|
content = content.replace(/<div class=\\"__boltThought__\\">.*?<\/div>/s, '');
|
||||||
if (contextOptimization) {
|
content = content.replace(/<think>.*?<\/think>/s, '');
|
||||||
content = simplifyBoltActions(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...message, content };
|
return { ...message, content };
|
||||||
}
|
}
|
||||||
@ -110,7 +109,7 @@ Below are all the files present in the project:
|
|||||||
${filePaths.join('\n')}
|
${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:
|
CONTEXT BUFFER:
|
||||||
---
|
---
|
||||||
${codeContext}
|
${codeContext}
|
||||||
@ -126,10 +125,14 @@ ${props.summary}
|
|||||||
---
|
---
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const lastMessage = processedMessages.pop();
|
if (props.messageSliceId) {
|
||||||
|
processedMessages = processedMessages.slice(props.messageSliceId);
|
||||||
|
} else {
|
||||||
|
const lastMessage = processedMessages.pop();
|
||||||
|
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
processedMessages = [lastMessage];
|
processedMessages = [lastMessage];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,10 +82,10 @@ export function createFilesContext(files: FileMap, useRelativePath?: boolean) {
|
|||||||
filePath = path.replace('/home/project/', '');
|
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[]) {
|
export function extractCurrentContext(messages: Message[]) {
|
||||||
|
@ -10,6 +10,7 @@ import { getFilePaths, selectContext } from '~/lib/.server/llm/select-context';
|
|||||||
import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
|
import type { ContextAnnotation, ProgressAnnotation } from '~/types/context';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { createSummary } from '~/lib/.server/llm/create-summary';
|
import { createSummary } from '~/lib/.server/llm/create-summary';
|
||||||
|
import { extractPropertiesFromMessage } from '~/lib/.server/llm/utils';
|
||||||
|
|
||||||
export async function action(args: ActionFunctionArgs) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
return chatAction(args);
|
return chatAction(args);
|
||||||
@ -70,15 +71,21 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
const filePaths = getFilePaths(files || {});
|
const filePaths = getFilePaths(files || {});
|
||||||
let filteredFiles: FileMap | undefined = undefined;
|
let filteredFiles: FileMap | undefined = undefined;
|
||||||
let summary: string | undefined = undefined;
|
let summary: string | undefined = undefined;
|
||||||
|
let messageSliceId = 0;
|
||||||
|
|
||||||
|
if (messages.length > 3) {
|
||||||
|
messageSliceId = messages.length - 3;
|
||||||
|
}
|
||||||
|
|
||||||
if (filePaths.length > 0 && contextOptimization) {
|
if (filePaths.length > 0 && contextOptimization) {
|
||||||
dataStream.writeData('HI ');
|
|
||||||
logger.debug('Generating Chat Summary');
|
logger.debug('Generating Chat Summary');
|
||||||
dataStream.writeMessageAnnotation({
|
dataStream.writeData({
|
||||||
type: 'progress',
|
type: 'progress',
|
||||||
value: progressCounter++,
|
label: 'summary',
|
||||||
message: 'Generating Chat Summary',
|
status: 'in-progress',
|
||||||
} as ProgressAnnotation);
|
order: progressCounter++,
|
||||||
|
message: 'Analysing Request',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
|
|
||||||
// Create a summary of the chat
|
// Create a summary of the chat
|
||||||
console.log(`Messages count: ${messages.length}`);
|
console.log(`Messages count: ${messages.length}`);
|
||||||
@ -99,6 +106,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
dataStream.writeData({
|
||||||
|
type: 'progress',
|
||||||
|
label: 'summary',
|
||||||
|
status: 'complete',
|
||||||
|
order: progressCounter++,
|
||||||
|
message: 'Analysis Complete',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
|
|
||||||
dataStream.writeMessageAnnotation({
|
dataStream.writeMessageAnnotation({
|
||||||
type: 'chatSummary',
|
type: 'chatSummary',
|
||||||
@ -108,11 +122,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
// Update context buffer
|
// Update context buffer
|
||||||
logger.debug('Updating Context Buffer');
|
logger.debug('Updating Context Buffer');
|
||||||
dataStream.writeMessageAnnotation({
|
dataStream.writeData({
|
||||||
type: 'progress',
|
type: 'progress',
|
||||||
value: progressCounter++,
|
label: 'context',
|
||||||
message: 'Updating Context Buffer',
|
status: 'in-progress',
|
||||||
} as ProgressAnnotation);
|
order: progressCounter++,
|
||||||
|
message: 'Determining Files to Read',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
|
|
||||||
// Select context files
|
// Select context files
|
||||||
console.log(`Messages count: ${messages.length}`);
|
console.log(`Messages count: ${messages.length}`);
|
||||||
@ -152,12 +168,15 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
}),
|
}),
|
||||||
} as ContextAnnotation);
|
} as ContextAnnotation);
|
||||||
|
|
||||||
dataStream.writeMessageAnnotation({
|
dataStream.writeData({
|
||||||
type: 'progress',
|
type: 'progress',
|
||||||
value: progressCounter++,
|
label: 'context',
|
||||||
message: 'Context Buffer Updated',
|
status: 'complete',
|
||||||
} as ProgressAnnotation);
|
order: progressCounter++,
|
||||||
logger.debug('Context Buffer Updated');
|
message: 'Code Files Selected',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
|
|
||||||
|
// logger.debug('Code Files Selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream the text
|
// Stream the text
|
||||||
@ -181,6 +200,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
totalTokens: cumulativeUsage.totalTokens,
|
totalTokens: cumulativeUsage.totalTokens,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
dataStream.writeData({
|
||||||
|
type: 'progress',
|
||||||
|
label: 'response',
|
||||||
|
status: 'complete',
|
||||||
|
order: progressCounter++,
|
||||||
|
message: 'Response Generated',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
// stream.close();
|
// stream.close();
|
||||||
@ -195,8 +221,14 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
|
logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`);
|
||||||
|
|
||||||
|
const lastUserMessage = messages.filter((x) => x.role == 'user').slice(-1)[0];
|
||||||
|
const { model, provider } = extractPropertiesFromMessage(lastUserMessage);
|
||||||
messages.push({ id: generateId(), role: 'assistant', content });
|
messages.push({ id: generateId(), role: 'assistant', content });
|
||||||
messages.push({ id: generateId(), role: 'user', content: CONTINUE_PROMPT });
|
messages.push({
|
||||||
|
id: generateId(),
|
||||||
|
role: 'user',
|
||||||
|
content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${CONTINUE_PROMPT}`,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await streamText({
|
const result = await streamText({
|
||||||
messages,
|
messages,
|
||||||
@ -207,6 +239,9 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
providerSettings,
|
providerSettings,
|
||||||
promptId,
|
promptId,
|
||||||
contextOptimization,
|
contextOptimization,
|
||||||
|
contextFiles: filteredFiles,
|
||||||
|
summary,
|
||||||
|
messageSliceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
result.mergeIntoDataStream(dataStream);
|
result.mergeIntoDataStream(dataStream);
|
||||||
@ -226,6 +261,14 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dataStream.writeData({
|
||||||
|
type: 'progress',
|
||||||
|
label: 'response',
|
||||||
|
status: 'in-progress',
|
||||||
|
order: progressCounter++,
|
||||||
|
message: 'Generating Response',
|
||||||
|
} satisfies ProgressAnnotation);
|
||||||
|
|
||||||
const result = await streamText({
|
const result = await streamText({
|
||||||
messages,
|
messages,
|
||||||
env: context.cloudflare?.env,
|
env: context.cloudflare?.env,
|
||||||
@ -237,6 +280,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|||||||
contextOptimization,
|
contextOptimization,
|
||||||
contextFiles: filteredFiles,
|
contextFiles: filteredFiles,
|
||||||
summary,
|
summary,
|
||||||
|
messageSliceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -7,6 +7,7 @@ import { MAX_TOKENS } from '~/lib/.server/llm/constants';
|
|||||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||||
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
|
||||||
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
|
|
||||||
export async function action(args: ActionFunctionArgs) {
|
export async function action(args: ActionFunctionArgs) {
|
||||||
return llmCallAction(args);
|
return llmCallAction(args);
|
||||||
@ -21,6 +22,8 @@ async function getModelList(options: {
|
|||||||
return llmManager.updateModelList(options);
|
return llmManager.updateModelList(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logger = createScopedLogger('api.llmcall');
|
||||||
|
|
||||||
async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
||||||
const { system, message, model, provider, streamOutput } = await request.json<{
|
const { system, message, model, provider, streamOutput } = await request.json<{
|
||||||
system: string;
|
system: string;
|
||||||
@ -106,6 +109,8 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
|||||||
throw new Error('Provider not found');
|
throw new Error('Provider not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`Generating response Provider: ${provider.name}, Model: ${modelDetails.name}`);
|
||||||
|
|
||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
system,
|
system,
|
||||||
messages: [
|
messages: [
|
||||||
@ -123,6 +128,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) {
|
|||||||
maxTokens: dynamicMaxTokens,
|
maxTokens: dynamicMaxTokens,
|
||||||
toolChoice: 'none',
|
toolChoice: 'none',
|
||||||
});
|
});
|
||||||
|
logger.info(`Generated response`);
|
||||||
|
|
||||||
return new Response(JSON.stringify(result), {
|
return new Response(JSON.stringify(result), {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -11,6 +11,8 @@ export type ContextAnnotation =
|
|||||||
|
|
||||||
export type ProgressAnnotation = {
|
export type ProgressAnnotation = {
|
||||||
type: 'progress';
|
type: 'progress';
|
||||||
value: number;
|
label: string;
|
||||||
|
status: 'in-progress' | 'complete';
|
||||||
|
order: number;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
@ -59,6 +59,7 @@ Instructions:
|
|||||||
5. If no perfect match exists, recommend the closest option
|
5. If no perfect match exists, recommend the closest option
|
||||||
|
|
||||||
Important: Provide only the selection tags in your response, no additional text.
|
Important: Provide only the selection tags in your response, no additional text.
|
||||||
|
MOST IMPORTANT: YOU DONT HAVE TIME TO THINK JUST START RESPONDING BASED ON HUNCH
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn'));
|
const templates: Template[] = STARTER_TEMPLATES.filter((t) => !t.name.includes('shadcn'));
|
||||||
|
Loading…
Reference in New Issue
Block a user