diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index be304c7bc..60076bd8b 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -1,6 +1,8 @@ import { memo } from 'react'; import { Markdown } from './Markdown'; import type { JSONValue } from 'ai'; +import type { ProgressAnnotation } from '~/types/context'; +import Popover from '~/components/ui/Popover'; interface AssistantMessageProps { content: string; @@ -10,7 +12,12 @@ interface AssistantMessageProps { export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => { const filteredAnnotations = (annotations?.filter( (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), - ) || []) as { type: string; value: any }[]; + ) || []) as { type: string; value: any } & { [key: string]: any }[]; + + let progressAnnotation: ProgressAnnotation[] = filteredAnnotations.filter( + (annotation) => annotation.type === 'progress', + ) as ProgressAnnotation[]; + progressAnnotation = progressAnnotation.sort((a, b) => b.value - a.value); const usage: { completionTokens: number; @@ -20,11 +27,18 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage return (
- {usage && ( -
- Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) + <> +
+ {progressAnnotation.length > 0 && ( + }>{progressAnnotation[0].message} + )} + {usage && ( +
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) +
+ )}
- )} + {content}
); diff --git a/app/components/ui/Popover.tsx b/app/components/ui/Popover.tsx new file mode 100644 index 000000000..d00bf976a --- /dev/null +++ b/app/components/ui/Popover.tsx @@ -0,0 +1,20 @@ +import * as Popover from '@radix-ui/react-popover'; +import type { PropsWithChildren, ReactNode } from 'react'; + +export default ({ children, trigger }: PropsWithChildren<{ trigger: ReactNode }>) => ( + + {trigger} + + + + {children} + + + + +); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 642918b22..af399c140 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,4 +1,4 @@ -import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'; +import type { AppLoadContext } from '@remix-run/cloudflare'; import { RemixServer } from '@remix-run/react'; import { isbot } from 'isbot'; import { renderToReadableStream } from 'react-dom/server'; @@ -10,7 +10,7 @@ export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext, + remixContext: any, _loadContext: AppLoadContext, ) { // await initializeModelList({}); diff --git a/app/lib/.server/llm/constants.ts b/app/lib/.server/llm/constants.ts index 7b3a0f245..cca1b19a9 100644 --- a/app/lib/.server/llm/constants.ts +++ b/app/lib/.server/llm/constants.ts @@ -3,3 +3,36 @@ export const MAX_TOKENS = 8000; // limits the number of model responses that can be returned in a single request export const MAX_RESPONSE_SEGMENTS = 2; + +export interface File { + type: 'file'; + content: string; + isBinary: boolean; +} + +export interface Folder { + type: 'folder'; +} + +type Dirent = File | Folder; + +export type FileMap = Record; + +export const IGNORE_PATTERNS = [ + 'node_modules/**', + '.git/**', + 'dist/**', + 'build/**', + '.next/**', + 'coverage/**', + '.cache/**', + '.vscode/**', + '.idea/**', + '**/*.log', + '**/.DS_Store', + '**/npm-debug.log*', + '**/yarn-debug.log*', + '**/yarn-error.log*', + '**/*lock.json', + '**/*lock.yml', +]; diff --git a/app/lib/.server/llm/create-summary.ts b/app/lib/.server/llm/create-summary.ts new file mode 100644 index 000000000..4d14a2791 --- /dev/null +++ b/app/lib/.server/llm/create-summary.ts @@ -0,0 +1,138 @@ +import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai'; +import type { IProviderSetting } from '~/types/model'; +import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants'; +import { extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils'; +import { createScopedLogger } from '~/utils/logger'; +import { LLMManager } from '~/lib/modules/llm/manager'; + +const logger = createScopedLogger('create-summary'); + +export async function createSummary(props: { + messages: Message[]; + env?: Env; + apiKeys?: Record; + providerSettings?: Record; + promptId?: string; + contextOptimization?: boolean; + onFinish?: (resp: GenerateTextResult>, never>) => void; +}) { + const { messages, env: serverEnv, apiKeys, providerSettings, contextOptimization, onFinish } = props; + let currentModel = DEFAULT_MODEL; + let currentProvider = DEFAULT_PROVIDER.name; + const processedMessages = messages.map((message) => { + if (message.role === 'user') { + const { model, provider, content } = extractPropertiesFromMessage(message); + currentModel = model; + currentProvider = provider; + + return { ...message, content }; + } else if (message.role == 'assistant') { + let content = message.content; + + if (contextOptimization) { + content = simplifyBoltActions(content); + } + + return { ...message, content }; + } + + return message; + }); + + const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER; + const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider); + let modelDetails = staticModels.find((m) => m.name === currentModel); + + if (!modelDetails) { + const modelsList = [ + ...(provider.staticModels || []), + ...(await LLMManager.getInstance().getModelListFromProvider(provider, { + apiKeys, + providerSettings, + serverEnv: serverEnv as any, + })), + ]; + + if (!modelsList.length) { + throw new Error(`No models found for provider ${provider.name}`); + } + + modelDetails = modelsList.find((m) => m.name === currentModel); + + if (!modelDetails) { + // Fallback to first model + logger.warn( + `MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`, + ); + modelDetails = modelsList[0]; + } + } + + let slicedMessages = processedMessages; + const { summary } = extractCurrentContext(processedMessages); + let summaryText: string | undefined = undefined; + let chatId: string | undefined = undefined; + + if (summary && summary.type === 'chatSummary') { + chatId = summary.chatId; + summaryText = `Below is the Chat Summary till now, this is chat summary before the conversation provided by the user +you should also use this as historical message while providing the response to the user. +${summary.summary}`; + + if (chatId) { + let index = 0; + + for (let i = 0; i < processedMessages.length; i++) { + if (processedMessages[i].id === chatId) { + index = i; + break; + } + } + slicedMessages = processedMessages.slice(index + 1); + } + } + + const extractTextContent = (message: Message) => + Array.isArray(message.content) + ? (message.content.find((item) => item.type === 'text')?.text as string) || '' + : message.content; + + // 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. + + ${summaryText} + + RULES: + * Only provide the summary of the chat till now. + * Do not provide any new information. + `, + prompt: ` +please provide a summary of the chat till now. +below is the latest chat: + +--- +${slicedMessages + .map((x) => { + return `---\n[${x.role}] ${extractTextContent(x)}\n---`; + }) + .join('\n')} +--- +`, + model: provider.getModelInstance({ + model: currentModel, + serverEnv, + apiKeys, + providerSettings, + }), + }); + + const response = resp.text; + + if (onFinish) { + onFinish(resp); + } + + return response; +} diff --git a/app/lib/.server/llm/select-context.ts b/app/lib/.server/llm/select-context.ts new file mode 100644 index 000000000..85780e42d --- /dev/null +++ b/app/lib/.server/llm/select-context.ts @@ -0,0 +1,233 @@ +import { generateText, type CoreTool, type GenerateTextResult, type Message } from 'ai'; +import ignore from 'ignore'; +import type { IProviderSetting } from '~/types/model'; +import { IGNORE_PATTERNS, type FileMap } from './constants'; +import { DEFAULT_MODEL, DEFAULT_PROVIDER, PROVIDER_LIST } from '~/utils/constants'; +import { createFilesContext, extractCurrentContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils'; +import { createScopedLogger } from '~/utils/logger'; +import { LLMManager } from '~/lib/modules/llm/manager'; + +// Common patterns to ignore, similar to .gitignore + +const ig = ignore().add(IGNORE_PATTERNS); +const logger = createScopedLogger('select-context'); + +export async function selectContext(props: { + messages: Message[]; + env?: Env; + apiKeys?: Record; + files: FileMap; + providerSettings?: Record; + promptId?: string; + contextOptimization?: boolean; + summary: string; + onFinish?: (resp: GenerateTextResult>, never>) => void; +}) { + const { messages, env: serverEnv, apiKeys, files, providerSettings, contextOptimization, summary, onFinish } = props; + let currentModel = DEFAULT_MODEL; + let currentProvider = DEFAULT_PROVIDER.name; + const processedMessages = messages.map((message) => { + if (message.role === 'user') { + const { model, provider, content } = extractPropertiesFromMessage(message); + currentModel = model; + currentProvider = provider; + + return { ...message, content }; + } else if (message.role == 'assistant') { + let content = message.content; + + if (contextOptimization) { + content = simplifyBoltActions(content); + } + + return { ...message, content }; + } + + return message; + }); + + const provider = PROVIDER_LIST.find((p) => p.name === currentProvider) || DEFAULT_PROVIDER; + const staticModels = LLMManager.getInstance().getStaticModelListFromProvider(provider); + let modelDetails = staticModels.find((m) => m.name === currentModel); + + if (!modelDetails) { + const modelsList = [ + ...(provider.staticModels || []), + ...(await LLMManager.getInstance().getModelListFromProvider(provider, { + apiKeys, + providerSettings, + serverEnv: serverEnv as any, + })), + ]; + + if (!modelsList.length) { + throw new Error(`No models found for provider ${provider.name}`); + } + + modelDetails = modelsList.find((m) => m.name === currentModel); + + if (!modelDetails) { + // Fallback to first model + logger.warn( + `MODEL [${currentModel}] not found in provider [${provider.name}]. Falling back to first model. ${modelsList[0].name}`, + ); + modelDetails = modelsList[0]; + } + } + + const { codeContext } = extractCurrentContext(processedMessages); + + let filePaths = getFilePaths(files || {}); + filePaths = filePaths.filter((x) => { + const relPath = x.replace('/home/project/', ''); + return !ig.ignores(relPath); + }); + + let context = ''; + const currrentFiles: string[] = []; + const contextFiles: FileMap = {}; + + if (codeContext?.type === 'codeContext') { + const codeContextFiles: string[] = codeContext.files; + Object.keys(files || {}).forEach((path) => { + let relativePath = path; + + if (path.startsWith('/home/project/')) { + relativePath = path.replace('/home/project/', ''); + } + + if (codeContextFiles.includes(relativePath)) { + contextFiles[relativePath] = files[path]; + currrentFiles.push(relativePath); + } + }); + context = createFilesContext(contextFiles); + } + + const summaryText = `Here is the summary of the chat till now: ${summary}`; + + const extractTextContent = (message: Message) => + Array.isArray(message.content) + ? (message.content.find((item) => item.type === 'text')?.text as string) || '' + : message.content; + + const lastUserMessage = processedMessages.filter((x) => x.role == 'user').pop(); + + if (!lastUserMessage) { + throw new Error('No user message found'); + } + + // 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. You have access to the following files: + + AVAILABLE FILES PATHS + --- + ${filePaths.map((path) => `- ${path}`).join('\n')} + --- + + You have following code loaded in the context buffer that you can refer to: + + CURRENT CONTEXT BUFFER + --- + ${context} + --- + + Now, you are given a task. You need to select the files that are relevant to the task from the list of files above. + + RESPONSE FORMAT: + your response shoudl be in following format: +--- + + + + +--- + * Your should start with and end with . + * You can include multiple and tags in the response. + * You should not include any other text in the response. + * You should not include any file that is not in the list of files above. + * You should not include any file that is already in the context buffer. + * If no changes are needed, you can leave the response empty updateContextBuffer tag. + `, + prompt: ` + ${summaryText} + + Users Question: ${extractTextContent(lastUserMessage)} + + update the context buffer with the files that are relevant to the task from the list of files above. + + CRITICAL RULES: + * Only include relevant files in the context buffer. + * context buffer should not include any file that is not in the list of files above. + * context buffer is extremlly expensive, so only include files that are absolutely necessary. + * If no changes are needed, you can leave the response empty updateContextBuffer tag. + * Only 5 files can be placed in the context buffer at a time. + * if the buffer is full, you need to exclude files that is not needed and include files that is relevent. + + `, + model: provider.getModelInstance({ + model: currentModel, + serverEnv, + apiKeys, + providerSettings, + }), + }); + + const response = resp.text; + const updateContextBuffer = response.match(/([\s\S]*?)<\/updateContextBuffer>/); + + if (!updateContextBuffer) { + throw new Error('Invalid response. Please follow the response format'); + } + + const includeFiles = + updateContextBuffer[1] + .match(/ x.replace(' x.replace(' { + delete contextFiles[path]; + }); + includeFiles.forEach((path) => { + let fullPath = path; + + if (!path.startsWith('/home/project/')) { + fullPath = `/home/project/${path}`; + } + + if (!filePaths.includes(fullPath)) { + throw new Error(`File ${path} is not in the list of files above.`); + } + + if (currrentFiles.includes(path)) { + return; + } + + filteredFiles[path] = files[fullPath]; + }); + + if (onFinish) { + onFinish(resp); + } + + return filteredFiles; + + // generateText({ +} + +export function getFilePaths(files: FileMap) { + let filePaths = Object.keys(files); + filePaths = filePaths.filter((x) => { + const relPath = x.replace('/home/project/', ''); + return !ig.ignores(relPath); + }); + + return filePaths; +} diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 7233e739b..374610c71 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -1,162 +1,48 @@ -import { convertToCoreMessages, streamText as _streamText } from 'ai'; -import { MAX_TOKENS } from './constants'; +import { convertToCoreMessages, streamText as _streamText, type Message } from 'ai'; +import { MAX_TOKENS, type FileMap } from './constants'; import { getSystemPrompt } from '~/lib/common/prompts/prompts'; -import { - DEFAULT_MODEL, - DEFAULT_PROVIDER, - MODEL_REGEX, - MODIFICATIONS_TAG_NAME, - PROVIDER_LIST, - PROVIDER_REGEX, - WORK_DIR, -} from '~/utils/constants'; -import ignore from 'ignore'; +import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODIFICATIONS_TAG_NAME, PROVIDER_LIST, WORK_DIR } from '~/utils/constants'; import type { IProviderSetting } from '~/types/model'; import { PromptLibrary } from '~/lib/common/prompt-library'; import { allowedHTMLElements } from '~/utils/markdown'; import { LLMManager } from '~/lib/modules/llm/manager'; import { createScopedLogger } from '~/utils/logger'; - -interface ToolResult { - toolCallId: string; - toolName: Name; - args: Args; - result: Result; -} - -interface Message { - role: 'user' | 'assistant'; - content: string; - toolInvocations?: ToolResult[]; - model?: string; -} +import { createFilesContext, extractPropertiesFromMessage, simplifyBoltActions } from './utils'; +import { getFilePaths } from './select-context'; export type Messages = Message[]; export type StreamingOptions = Omit[0], 'model'>; -export interface File { - type: 'file'; - content: string; - isBinary: boolean; -} - -export interface Folder { - type: 'folder'; -} - -type Dirent = File | Folder; - -export type FileMap = Record; - -export function simplifyBoltActions(input: string): string { - // Using regex to match boltAction tags that have type="file" - const regex = /(]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g; - - // Replace each matching occurrence - return input.replace(regex, (_0, openingTag, _2, closingTag) => { - return `${openingTag}\n ...\n ${closingTag}`; - }); -} - -// Common patterns to ignore, similar to .gitignore -const IGNORE_PATTERNS = [ - 'node_modules/**', - '.git/**', - 'dist/**', - 'build/**', - '.next/**', - 'coverage/**', - '.cache/**', - '.vscode/**', - '.idea/**', - '**/*.log', - '**/.DS_Store', - '**/npm-debug.log*', - '**/yarn-debug.log*', - '**/yarn-error.log*', - '**/*lock.json', - '**/*lock.yml', -]; -const ig = ignore().add(IGNORE_PATTERNS); - -function createFilesContext(files: FileMap) { - let filePaths = Object.keys(files); - filePaths = filePaths.filter((x) => { - const relPath = x.replace('/home/project/', ''); - return !ig.ignores(relPath); - }); - - const fileContexts = filePaths - .filter((x) => files[x] && files[x].type == 'file') - .map((path) => { - const dirent = files[path]; - - if (!dirent || dirent.type == 'folder') { - return ''; - } - - return `\n${dirent.content}\n`; - }); - - return `Below are the code files present in the webcontainer:\n \n${fileContexts.join('\n\n')}\n`; -} - -function extractPropertiesFromMessage(message: Message): { model: string; provider: string; content: string } { - const textContent = Array.isArray(message.content) - ? message.content.find((item) => item.type === 'text')?.text || '' - : message.content; - - const modelMatch = textContent.match(MODEL_REGEX); - const providerMatch = textContent.match(PROVIDER_REGEX); - - /* - * Extract model - * const modelMatch = message.content.match(MODEL_REGEX); - */ - const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL; - - /* - * Extract provider - * const providerMatch = message.content.match(PROVIDER_REGEX); - */ - const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name; - - const cleanedContent = Array.isArray(message.content) - ? message.content.map((item) => { - if (item.type === 'text') { - return { - type: 'text', - text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''), - }; - } - - return item; // Preserve image_url and other types as is - }) - : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); - - return { model, provider, content: cleanedContent }; -} - const logger = createScopedLogger('stream-text'); export async function streamText(props: { - messages: Messages; - env: Env; + messages: Omit[]; + env?: Env; options?: StreamingOptions; apiKeys?: Record; files?: FileMap; providerSettings?: Record; promptId?: string; contextOptimization?: boolean; + contextFiles?: FileMap; + summary?: string; }) { - const { messages, env: serverEnv, options, apiKeys, files, providerSettings, promptId, contextOptimization } = props; - - // console.log({serverEnv}); - + const { + messages, + env: serverEnv, + options, + apiKeys, + files, + providerSettings, + promptId, + contextOptimization, + contextFiles, + summary, + } = props; let currentModel = DEFAULT_MODEL; let currentProvider = DEFAULT_PROVIDER.name; - const processedMessages = messages.map((message) => { + let processedMessages = messages.map((message) => { if (message.role === 'user') { const { model, provider, content } = extractPropertiesFromMessage(message); currentModel = model; @@ -214,13 +100,44 @@ export async function streamText(props: { modificationTagName: MODIFICATIONS_TAG_NAME, }) ?? getSystemPrompt(); - if (files && contextOptimization) { - const codeContext = createFilesContext(files); - systemPrompt = `${systemPrompt}\n\n ${codeContext}`; + if (files && contextFiles && contextOptimization) { + const codeContext = createFilesContext(contextFiles, true); + const filePaths = getFilePaths(files); + + systemPrompt = `${systemPrompt} +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. +CONTEXT BUFFER: +--- +${codeContext} +--- +`; + + if (summary) { + systemPrompt = `${systemPrompt} + below is the chat history till now +CHAT SUMMARY: +--- +${props.summary} +--- +`; + + const lastMessage = processedMessages.pop(); + + if (lastMessage) { + processedMessages = [lastMessage]; + } + } } logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`); + // console.log(systemPrompt,processedMessages); + return await _streamText({ model: provider.getModelInstance({ model: modelDetails.name, diff --git a/app/lib/.server/llm/utils.ts b/app/lib/.server/llm/utils.ts new file mode 100644 index 000000000..9aac0891f --- /dev/null +++ b/app/lib/.server/llm/utils.ts @@ -0,0 +1,128 @@ +import { type Message } from 'ai'; +import { DEFAULT_MODEL, DEFAULT_PROVIDER, MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; +import { IGNORE_PATTERNS, type FileMap } from './constants'; +import ignore from 'ignore'; +import type { ContextAnnotation } from '~/types/context'; + +export function extractPropertiesFromMessage(message: Omit): { + model: string; + provider: string; + content: string; +} { + const textContent = Array.isArray(message.content) + ? message.content.find((item) => item.type === 'text')?.text || '' + : message.content; + + const modelMatch = textContent.match(MODEL_REGEX); + const providerMatch = textContent.match(PROVIDER_REGEX); + + /* + * Extract model + * const modelMatch = message.content.match(MODEL_REGEX); + */ + const model = modelMatch ? modelMatch[1] : DEFAULT_MODEL; + + /* + * Extract provider + * const providerMatch = message.content.match(PROVIDER_REGEX); + */ + const provider = providerMatch ? providerMatch[1] : DEFAULT_PROVIDER.name; + + const cleanedContent = Array.isArray(message.content) + ? message.content.map((item) => { + if (item.type === 'text') { + return { + type: 'text', + text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''), + }; + } + + return item; // Preserve image_url and other types as is + }) + : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); + + return { model, provider, content: cleanedContent }; +} + +export function simplifyBoltActions(input: string): string { + // Using regex to match boltAction tags that have type="file" + const regex = /(]*type="file"[^>]*>)([\s\S]*?)(<\/boltAction>)/g; + + // Replace each matching occurrence + return input.replace(regex, (_0, openingTag, _2, closingTag) => { + return `${openingTag}\n ...\n ${closingTag}`; + }); +} + +export function createFilesContext(files: FileMap, useRelativePath?: boolean) { + const ig = ignore().add(IGNORE_PATTERNS); + let filePaths = Object.keys(files); + filePaths = filePaths.filter((x) => { + const relPath = x.replace('/home/project/', ''); + return !ig.ignores(relPath); + }); + + const fileContexts = filePaths + .filter((x) => files[x] && files[x].type == 'file') + .map((path) => { + const dirent = files[path]; + + if (!dirent || dirent.type == 'folder') { + return ''; + } + + const codeWithLinesNumbers = dirent.content + .split('\n') + // .map((v, i) => `${i + 1}|${v}`) + .join('\n'); + + let filePath = path; + + if (useRelativePath) { + filePath = path.replace('/home/project/', ''); + } + + return `\n${codeWithLinesNumbers}\n`; + }); + + return `${fileContexts.join('\n\n')}\n\n`; +} + +export function extractCurrentContext(messages: Message[]) { + const lastAssistantMessage = messages.filter((x) => x.role == 'assistant').slice(-1)[0]; + + if (!lastAssistantMessage) { + return { summary: undefined, codeContext: undefined }; + } + + let summary: ContextAnnotation | undefined; + let codeContext: ContextAnnotation | undefined; + + if (!lastAssistantMessage.annotations?.length) { + return { summary: undefined, codeContext: undefined }; + } + + for (let i = 0; i < lastAssistantMessage.annotations.length; i++) { + const annotation = lastAssistantMessage.annotations[i]; + + if (!annotation || typeof annotation !== 'object') { + continue; + } + + if (!(annotation as any).type) { + continue; + } + + const annotationObject = annotation as any; + + if (annotationObject.type === 'codeContext') { + codeContext = annotationObject; + break; + } else if (annotationObject.type === 'chatSummary') { + summary = annotationObject; + break; + } + } + + return { summary, codeContext }; +} diff --git a/app/lib/modules/llm/base-provider.ts b/app/lib/modules/llm/base-provider.ts index 1e3b1c8c3..9cb23403d 100644 --- a/app/lib/modules/llm/base-provider.ts +++ b/app/lib/modules/llm/base-provider.ts @@ -111,7 +111,7 @@ export abstract class BaseProvider implements ProviderInfo { abstract getModelInstance(options: { model: string; - serverEnv: Env; + serverEnv?: Env; apiKeys?: Record; providerSettings?: Record; }): LanguageModelV1; diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 5c8c298be..aacaacf98 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -1,11 +1,15 @@ import { type ActionFunctionArgs } from '@remix-run/cloudflare'; -import { createDataStream } from 'ai'; -import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants'; +import { createDataStream, generateId } from 'ai'; +import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS, type FileMap } from '~/lib/.server/llm/constants'; import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts'; import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text'; import SwitchableStream from '~/lib/.server/llm/switchable-stream'; import type { IProviderSetting } from '~/types/model'; import { createScopedLogger } from '~/utils/logger'; +import { getFilePaths, selectContext } from '~/lib/.server/llm/select-context'; +import type { ContextAnnotation, ProgressAnnotation } from '~/types/context'; +import { WORK_DIR } from '~/utils/constants'; +import { createSummary } from '~/lib/.server/llm/create-summary'; export async function action(args: ActionFunctionArgs) { return chatAction(args); @@ -52,23 +56,121 @@ async function chatAction({ context, request }: ActionFunctionArgs) { promptTokens: 0, totalTokens: 0, }; + const encoder: TextEncoder = new TextEncoder(); + let progressCounter: number = 1; try { - const options: StreamingOptions = { - toolChoice: 'none', - onFinish: async ({ text: content, finishReason, usage }) => { - logger.debug('usage', JSON.stringify(usage)); - - if (usage) { - cumulativeUsage.completionTokens += usage.completionTokens || 0; - cumulativeUsage.promptTokens += usage.promptTokens || 0; - cumulativeUsage.totalTokens += usage.totalTokens || 0; + const totalMessageContent = messages.reduce((acc, message) => acc + message.content, ''); + logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`); + + const dataStream = createDataStream({ + async execute(dataStream) { + const filePaths = getFilePaths(files || {}); + let filteredFiles: FileMap | undefined = undefined; + let summary: string | undefined = undefined; + + if (filePaths.length > 0 && contextOptimization) { + dataStream.writeData('HI '); + logger.debug('Generating Chat Summary'); + dataStream.writeMessageAnnotation({ + type: 'progress', + value: progressCounter++, + message: 'Generating Chat Summary', + } as ProgressAnnotation); + + // Create a summary of the chat + console.log(`Messages count: ${messages.length}`); + + summary = await createSummary({ + messages: [...messages], + env: context.cloudflare?.env, + apiKeys, + providerSettings, + promptId, + contextOptimization, + onFinish(resp) { + if (resp.usage) { + logger.debug('createSummary token usage', JSON.stringify(resp.usage)); + cumulativeUsage.completionTokens += resp.usage.completionTokens || 0; + cumulativeUsage.promptTokens += resp.usage.promptTokens || 0; + cumulativeUsage.totalTokens += resp.usage.totalTokens || 0; + } + }, + }); + + dataStream.writeMessageAnnotation({ + type: 'chatSummary', + summary, + chatId: messages.slice(-1)?.[0]?.id, + } as ContextAnnotation); + + // Update context buffer + logger.debug('Updating Context Buffer'); + dataStream.writeMessageAnnotation({ + type: 'progress', + value: progressCounter++, + message: 'Updating Context Buffer', + } as ProgressAnnotation); + + // Select context files + console.log(`Messages count: ${messages.length}`); + filteredFiles = await selectContext({ + messages: [...messages], + env: context.cloudflare?.env, + apiKeys, + files, + providerSettings, + promptId, + contextOptimization, + summary, + onFinish(resp) { + if (resp.usage) { + logger.debug('selectContext token usage', JSON.stringify(resp.usage)); + cumulativeUsage.completionTokens += resp.usage.completionTokens || 0; + cumulativeUsage.promptTokens += resp.usage.promptTokens || 0; + cumulativeUsage.totalTokens += resp.usage.totalTokens || 0; + } + }, + }); + + if (filteredFiles) { + logger.debug(`files in context : ${JSON.stringify(Object.keys(filteredFiles))}`); + } + + dataStream.writeMessageAnnotation({ + type: 'codeContext', + files: Object.keys(filteredFiles).map((key) => { + let path = key; + + if (path.startsWith(WORK_DIR)) { + path = path.replace(WORK_DIR, ''); + } + + return path; + }), + } as ContextAnnotation); + + dataStream.writeMessageAnnotation({ + type: 'progress', + value: progressCounter++, + message: 'Context Buffer Updated', + } as ProgressAnnotation); + logger.debug('Context Buffer Updated'); } - if (finishReason !== 'length') { - const encoder = new TextEncoder(); - const usageStream = createDataStream({ - async execute(dataStream) { + // Stream the text + const options: StreamingOptions = { + toolChoice: 'none', + onFinish: async ({ text: content, finishReason, usage }) => { + logger.debug('usage', JSON.stringify(usage)); + + if (usage) { + cumulativeUsage.completionTokens += usage.completionTokens || 0; + cumulativeUsage.promptTokens += usage.promptTokens || 0; + cumulativeUsage.totalTokens += usage.totalTokens || 0; + } + + if (finishReason !== 'length') { dataStream.writeMessageAnnotation({ type: 'usage', value: { @@ -77,80 +179,89 @@ async function chatAction({ context, request }: ActionFunctionArgs) { totalTokens: cumulativeUsage.totalTokens, }, }); - }, - onError: (error: any) => `Custom error: ${error.message}`, - }).pipeThrough( - new TransformStream({ - transform: (chunk, controller) => { - // Convert the string stream to a byte stream - const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); - controller.enqueue(encoder.encode(str)); - }, - }), - ); - await stream.switchSource(usageStream); - await new Promise((resolve) => setTimeout(resolve, 0)); - stream.close(); + await new Promise((resolve) => setTimeout(resolve, 0)); - return; - } + // stream.close(); + return; + } - if (stream.switches >= MAX_RESPONSE_SEGMENTS) { - throw Error('Cannot continue message: Maximum segments reached'); - } + if (stream.switches >= MAX_RESPONSE_SEGMENTS) { + throw Error('Cannot continue message: Maximum segments reached'); + } + + const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches; + + logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`); + + messages.push({ id: generateId(), role: 'assistant', content }); + messages.push({ id: generateId(), role: 'user', content: CONTINUE_PROMPT }); + + const result = await streamText({ + messages, + env: context.cloudflare?.env, + options, + apiKeys, + files, + providerSettings, + promptId, + contextOptimization, + }); + + result.mergeIntoDataStream(dataStream); - const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches; + (async () => { + for await (const part of result.fullStream) { + if (part.type === 'error') { + const error: any = part.error; + logger.error(`${error}`); - logger.info(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`); + return; + } + } + })(); - messages.push({ role: 'assistant', content }); - messages.push({ role: 'user', content: CONTINUE_PROMPT }); + return; + }, + }; const result = await streamText({ messages, - env: context.cloudflare.env, + env: context.cloudflare?.env, options, apiKeys, files, providerSettings, promptId, contextOptimization, + contextFiles: filteredFiles, + summary, }); - stream.switchSource(result.toDataStream()); + (async () => { + for await (const part of result.fullStream) { + if (part.type === 'error') { + const error: any = part.error; + logger.error(`${error}`); - return; - }, - }; - const totalMessageContent = messages.reduce((acc, message) => acc + message.content, ''); - logger.debug(`Total message length: ${totalMessageContent.split(' ').length}, words`); - - const result = await streamText({ - messages, - env: context.cloudflare.env, - options, - apiKeys, - files, - providerSettings, - promptId, - contextOptimization, - }); + return; + } + } + })(); - (async () => { - for await (const part of result.fullStream) { - if (part.type === 'error') { - const error: any = part.error; - logger.error(`${error}`); - - return; - } - } - })(); - - stream.switchSource(result.toDataStream()); + result.mergeIntoDataStream(dataStream); + }, + onError: (error: any) => `Custom error: ${error.message}`, + }).pipeThrough( + new TransformStream({ + transform: (chunk, controller) => { + // Convert the string stream to a byte stream + const str = typeof chunk === 'string' ? chunk : JSON.stringify(chunk); + controller.enqueue(encoder.encode(str)); + }, + }), + ); - // return createrespo - return new Response(stream.readable, { + return new Response(dataStream, { status: 200, headers: { 'Content-Type': 'text/event-stream; charset=utf-8', diff --git a/app/routes/api.enhancer.ts b/app/routes/api.enhancer.ts index 5f6db1f03..c1154bce3 100644 --- a/app/routes/api.enhancer.ts +++ b/app/routes/api.enhancer.ts @@ -74,7 +74,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) { `, }, ], - env: context.cloudflare.env, + env: context.cloudflare?.env as any, apiKeys, providerSettings, }); diff --git a/app/routes/api.llmcall.ts b/app/routes/api.llmcall.ts index a4a775519..5dd4c098f 100644 --- a/app/routes/api.llmcall.ts +++ b/app/routes/api.llmcall.ts @@ -63,7 +63,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) { content: `${message}`, }, ], - env: context.cloudflare.env, + env: context.cloudflare?.env as any, apiKeys, providerSettings, }); @@ -91,7 +91,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) { } } else { try { - const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare.env as any }); + const models = await getModelList({ apiKeys, providerSettings, serverEnv: context.cloudflare?.env as any }); const modelDetails = models.find((m: ModelInfo) => m.name === model); if (!modelDetails) { @@ -116,7 +116,7 @@ async function llmCallAction({ context, request }: ActionFunctionArgs) { ], model: providerInfo.getModelInstance({ model: modelDetails.name, - serverEnv: context.cloudflare.env as any, + serverEnv: context.cloudflare?.env as any, apiKeys, providerSettings, }), diff --git a/app/styles/variables.scss b/app/styles/variables.scss index 38967b117..d67aeeee2 100644 --- a/app/styles/variables.scss +++ b/app/styles/variables.scss @@ -219,7 +219,7 @@ --header-height: 54px; --chat-max-width: 37rem; --chat-min-width: 640px; - --workbench-width: min(calc(100% - var(--chat-min-width)), 1536px); + --workbench-width: min(calc(100% - var(--chat-min-width)), 2536px); --workbench-inner-width: var(--workbench-width); --workbench-left: calc(100% - var(--workbench-width)); diff --git a/app/types/context.ts b/app/types/context.ts new file mode 100644 index 000000000..4d8ea02b7 --- /dev/null +++ b/app/types/context.ts @@ -0,0 +1,16 @@ +export type ContextAnnotation = + | { + type: 'codeContext'; + files: string[]; + } + | { + type: 'chatSummary'; + summary: string; + chatId: string; + }; + +export type ProgressAnnotation = { + type: 'progress'; + value: number; + message: string; +}; diff --git a/package.json b/package.json index 5788c0030..c64b613f5 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "node": ">=18.18.0" }, "dependencies": { + "@ai-sdk/amazon-bedrock": "1.0.6", "@ai-sdk/anthropic": "^0.0.39", "@ai-sdk/cohere": "^1.0.3", "@ai-sdk/google": "^0.0.52", "@ai-sdk/mistral": "^0.0.43", "@ai-sdk/openai": "^0.0.66", - "@ai-sdk/amazon-bedrock": "1.0.6", "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-cpp": "^6.0.2", @@ -62,6 +62,7 @@ "@radix-ui/react-context-menu": "^2.2.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f625a94cb..78cf90269 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1647,6 +1650,9 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-arrow@1.1.0': resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -1660,6 +1666,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1682,6 +1701,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context-menu@2.2.2': resolution: {integrity: sha512-99EatSTpW+hRYHt7m8wdDlLtkmTovEe8Z/hnxUPV+SKuuNL5HWNhQI4QSdjZqNSgXHay2z4M3Dym73j9p2Gx5Q==} peerDependencies: @@ -1748,6 +1776,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dropdown-menu@2.1.2': resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} peerDependencies: @@ -1783,6 +1824,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1805,6 +1859,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.4': + resolution: {integrity: sha512-aUACAkXx8LaFymDma+HQVji7WhvEhpFJ7+qPz17Nf4lLZqtreGOFRiNQWQmhzp7kEWg9cOyyQJpdIMUMPc/CPw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.0': resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: @@ -1818,6 +1885,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.2': resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} peerDependencies: @@ -1831,6 +1911,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.1': resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} peerDependencies: @@ -1844,6 +1937,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -1857,6 +1963,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -1892,6 +2011,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.1.1': resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==} peerDependencies: @@ -4985,6 +5113,16 @@ packages: '@types/react': optional: true + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.6.0: resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} engines: {node: '>=10'} @@ -4995,6 +5133,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-resizable-panels@2.1.7: resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==} peerDependencies: @@ -5024,6 +5172,16 @@ packages: '@types/react': optional: true + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-toastify@10.0.6: resolution: {integrity: sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==} peerDependencies: @@ -5804,6 +5962,16 @@ packages: '@types/react': optional: true + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -7649,6 +7817,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7658,6 +7828,15 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-arrow@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7676,6 +7855,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-context-menu@2.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7743,6 +7928,19 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7775,6 +7973,17 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-id@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7808,6 +8017,29 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-popover@1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.2(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7826,6 +8058,24 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-popper@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -7836,6 +8086,16 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7846,6 +8106,16 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7855,6 +8125,15 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7888,6 +8167,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-slot@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -11850,6 +12136,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + react-remove-scroll-bar@2.3.8(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.12)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + react-remove-scroll@2.6.0(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -11861,6 +12155,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + react-remove-scroll@2.6.2(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -11887,6 +12192,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + react-style-singleton@2.2.3(@types/react@18.3.12)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + react-toastify@10.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 2.1.1 @@ -12737,6 +13050,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): dependencies: detect-node-es: 1.1.0