From e78a5b0a050818454344020b1682c07c1051f932 Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Mon, 18 Nov 2024 19:55:28 -0500 Subject: [PATCH 01/12] image-upload --- app/components/chat/BaseChat.tsx | 121 ++++++++++++++++------ app/components/chat/Chat.client.tsx | 42 +++++++- app/components/chat/FilePreview.tsx | 40 +++++++ app/components/chat/SendButton.client.tsx | 5 +- app/components/chat/UserMessage.tsx | 27 ++++- app/components/sidebar/Menu.client.tsx | 2 +- app/components/workbench/EditorPanel.tsx | 9 +- app/components/workbench/FileTree.tsx | 2 +- app/lib/.server/llm/stream-text.ts | 41 ++++++-- app/routes/api.chat.ts | 17 ++- 10 files changed, 244 insertions(+), 62 deletions(-) create mode 100644 app/components/chat/FilePreview.tsx diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 629c5cb..e0cd928 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -17,6 +17,8 @@ import Cookies from 'js-cookie'; import styles from './BaseChat.module.scss'; import type { ProviderInfo } from '~/utils/types'; +import FilePreview from './FilePreview'; + const EXAMPLE_PROMPTS = [ { text: 'Build a todo app in React using Tailwind' }, { text: 'Build a simple blog using Astro' }, @@ -33,7 +35,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov { - setProvider(providerList.find(p => p.name === e.target.value)); + setProvider(providerList.find((p) => p.name === e.target.value)); const firstModel = [...modelList].find((m) => m.provider == e.target.value); setModel(firstModel ? firstModel.name : ''); }} @@ -51,7 +51,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov key={provider?.name} value={model} onChange={(e) => setModel(e.target.value)} - style={{ maxWidth: "70%" }} + style={{ maxWidth: '70%' }} className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all" > {[...modelList] @@ -93,32 +93,34 @@ interface BaseChatProps { setImageDataList?: (dataList: string[]) => void; } export const BaseChat = React.forwardRef( - ({ - textareaRef, - messageRef, - scrollRef, - showChat, - chatStarted = false, - isStreaming = false, - model, - setModel, - provider, - setProvider, - input = '', - enhancingPrompt, - handleInputChange, - promptEnhanced, - enhancePrompt, - sendMessage, - handleStop, - uploadedFiles, - setUploadedFiles, - imageDataList, - setImageDataList, - messages, - children, // Add this - }, ref) => { - console.log(provider); + ( + { + textareaRef, + messageRef, + scrollRef, + showChat = true, + chatStarted = false, + isStreaming = false, + model, + setModel, + provider, + setProvider, + input = '', + enhancingPrompt, + handleInputChange, + promptEnhanced, + enhancePrompt, + sendMessage, + handleStop, + uploadedFiles, + setUploadedFiles, + imageDataList, + setImageDataList, + messages, + children, // Add this + }, + ref, + ) => { const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; const [apiKeys, setApiKeys] = useState>({}); const [modelList, setModelList] = useState(MODEL_LIST); @@ -139,7 +141,7 @@ export const BaseChat = React.forwardRef( Cookies.remove('apiKeys'); } - initializeModelList().then(modelList => { + initializeModelList().then((modelList) => { setModelList(modelList); }); }, []); @@ -239,12 +241,13 @@ export const BaseChat = React.forwardRef( setProvider={setProvider} providerList={PROVIDER_LIST} /> - {provider && + {provider && ( updateApiKey(provider.name, key)} - />} + /> + )} ( className="transition-all" onClick={() => handleFileUpload()} > -
+
( ); }, ); - - - diff --git a/app/components/chat/FilePreview.tsx b/app/components/chat/FilePreview.tsx index 378ada6..31fd11b 100644 --- a/app/components/chat/FilePreview.tsx +++ b/app/components/chat/FilePreview.tsx @@ -1,23 +1,22 @@ -// FilePreview.tsx +// Remove the lucide-react import import React from 'react'; -import { X } from 'lucide-react'; +// Rest of the interface remains the same interface FilePreviewProps { files: File[]; - imageDataList: string[]; // or imagePreviews: string[] + imageDataList: string[]; onRemove: (index: number) => void; } const FilePreview: React.FC = ({ files, imageDataList, onRemove }) => { if (!files || files.length === 0) { - return null; // Or render a placeholder if desired + return null; } return ( -
{/* Add horizontal scrolling if needed */} +
{files.map((file, index) => (
- {/* Display image preview or file icon */} {imageDataList[index] && (
{file.name} @@ -26,7 +25,7 @@ const FilePreview: React.FC = ({ files, imageDataList, onRemov className="absolute -top-2 -right-2 z-10 bg-white rounded-full p-1 shadow-md hover:bg-gray-100" >
- +
diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index a57d4fa..d7e1228 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -21,9 +21,6 @@ export function UserMessage({ content }: UserMessageProps) { ); } -// function sanitizeUserMessage(content: string) { -// return content.replace(modificationsRegex, '').replace(MODEL_REGEX, 'Using: $1').replace(PROVIDER_REGEX, ' ($1)\n\n').trim(); -// } function sanitizeUserMessage(content: string | Array<{type: string, text?: string, image_url?: {url: string}}>) { if (Array.isArray(content)) { return content.map(item => { diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index a50c28e..1603396 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -45,12 +45,12 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid if (item.type === 'text') { return { type: 'text', - text: item.text?.replace(/\[Model:.*?\]\n\n/, '').replace(/\[Provider:.*?\]\n\n/, '') + text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '') }; } return item; // Preserve image_url and other types as is }) - : textContent.replace(/\[Model:.*?\]\n\n/, '').replace(/\[Provider:.*?\]\n\n/, ''); + : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); return { model, provider, content: cleanedContent }; } @@ -80,16 +80,6 @@ export function streamText( return message; // No changes for non-user messages }); - // const modelConfig = getModel(currentProvider, currentModel, env, apiKeys); - // const coreMessages = convertToCoreMessages(processedMessages); - - // console.log('Debug streamText:', JSON.stringify({ - // model: modelConfig, - // messages: processedMessages, - // coreMessages: coreMessages, - // system: getSystemPrompt() - // }, null, 2)); - return _streamText({ model: getModel(currentProvider, currentModel, env, apiKeys), system: getSystemPrompt(), diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 284fccb..8fdb3d7 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -30,15 +30,15 @@ function parseCookies(cookieHeader) { } async function chatAction({ context, request }: ActionFunctionArgs) { - // console.log('=== API CHAT LOGGING START ==='); - // console.log('Request received:', request.url); - + const { messages, imageData } = await request.json<{ messages: Messages, imageData?: string[] }>(); const cookieHeader = request.headers.get("Cookie"); + + // Parse the cookie's value (returns an object or null if no cookie exists) const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || "{}"); const stream = new SwitchableStream(); @@ -71,13 +71,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) { const result = await streamText(messages, context.cloudflare.env, options, apiKeys); - // console.log('=== API CHAT LOGGING START ==='); - // console.log('StreamText:', JSON.stringify({ - // messages, - // result, - // }, null, 2)); - // console.log('=== API CHAT LOGGING END ==='); - stream.switchSource(result.toAIStream()); return new Response(stream.readable, { diff --git a/package.json b/package.json index 56a9e72..40ede0f 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "jose": "^5.6.3", "js-cookie": "^3.0.5", "jszip": "^3.10.1", - "lucide-react": "^0.460.0", "nanostores": "^0.10.3", "ollama-ai-provider": "^0.15.2", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ead147e..4158d19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,9 +155,6 @@ importers: jszip: specifier: ^3.10.1 version: 3.10.1 - lucide-react: - specifier: ^0.460.0 - version: 0.460.0(react@18.3.1) nanostores: specifier: ^0.10.3 version: 0.10.3 @@ -3674,11 +3671,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@0.460.0: - resolution: {integrity: sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc - magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -9492,10 +9484,6 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@0.460.0(react@18.3.1): - dependencies: - react: 18.3.1 - magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 From 76713c2e6e5b2518f9d8d995647204e4ab66c6b9 Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Wed, 20 Nov 2024 17:56:07 -0500 Subject: [PATCH 04/12] fixes for PR #332 --- app/components/chat/UserMessage.tsx | 21 +++++---------------- app/components/workbench/EditorPanel.tsx | 9 +++++---- app/lib/.server/llm/stream-text.ts | 5 +++++ 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index d7e1228..c5d9c9b 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -9,10 +9,7 @@ interface UserMessageProps { } export function UserMessage({ content }: UserMessageProps) { - const sanitizedContent = sanitizeUserMessage(content); - const textContent = Array.isArray(sanitizedContent) - ? sanitizedContent.find(item => item.type === 'text')?.text || '' - : sanitizedContent; + const textContent = sanitizeUserMessage(content); return (
@@ -23,17 +20,9 @@ export function UserMessage({ content }: UserMessageProps) { function sanitizeUserMessage(content: string | Array<{type: string, text?: string, image_url?: {url: string}}>) { if (Array.isArray(content)) { - return content.map(item => { - if (item.type === 'text') { - return { - type: 'text', - text: item.text?.replace(/\[Model:.*?\]\n\n/, '').replace(/\[Provider:.*?\]\n\n/, '') - }; - } - return item; // Keep image_url items unchanged - }); + const textItem = content.find(item => item.type === 'text'); + return textItem?.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '') || ''; } - // Handle legacy string content - return content.replace(/\[Model:.*?\]\n\n/, '').replace(/\[Provider:.*?\]\n\n/, ''); -} + return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); +} \ No newline at end of file diff --git a/app/components/workbench/EditorPanel.tsx b/app/components/workbench/EditorPanel.tsx index e789f1d..a9c9d33 100644 --- a/app/components/workbench/EditorPanel.tsx +++ b/app/components/workbench/EditorPanel.tsx @@ -23,6 +23,7 @@ import { isMobile } from '~/utils/mobile'; import { FileBreadcrumb } from './FileBreadcrumb'; import { FileTree } from './FileTree'; import { Terminal, type TerminalRef } from './terminal/Terminal'; +import React from 'react'; interface EditorPanelProps { files?: FileMap; @@ -203,7 +204,7 @@ export const EditorPanel = memo( const isActive = activeTerminal === index; return ( - <> + {index == 0 ? ( - + )} - + ); })} {terminalCount < MAX_TERMINALS && } diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 1603396..7951512 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -80,6 +80,11 @@ export function streamText( return message; // No changes for non-user messages }); + console.log('Stream Text:', JSON.stringify({ + model: getModel(currentProvider, currentModel, env, apiKeys), + messages: convertToCoreMessages(processedMessages), + })); + return _streamText({ model: getModel(currentProvider, currentModel, env, apiKeys), system: getSystemPrompt(), From 302cd28775b5abe214817c2b7790a313b2f8f34f Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Wed, 20 Nov 2024 19:35:57 -0500 Subject: [PATCH 05/12] . --- app/lib/.server/llm/stream-text.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 7951512..1603396 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -80,11 +80,6 @@ export function streamText( return message; // No changes for non-user messages }); - console.log('Stream Text:', JSON.stringify({ - model: getModel(currentProvider, currentModel, env, apiKeys), - messages: convertToCoreMessages(processedMessages), - })); - return _streamText({ model: getModel(currentProvider, currentModel, env, apiKeys), system: getSystemPrompt(), From 937ba7e61b9ba45d5283fbac3e8a34bd39f7d641 Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Thu, 21 Nov 2024 00:17:06 -0500 Subject: [PATCH 06/12] model pickup --- app/lib/.server/llm/stream-text.ts | 2 ++ app/routes/api.chat.ts | 8 ++++++-- app/utils/constants.ts | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 1603396..3b563ea 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -64,6 +64,8 @@ export function streamText( let currentModel = DEFAULT_MODEL; let currentProvider = DEFAULT_PROVIDER; + console.log('StreamText:', JSON.stringify(messages)); + const processedMessages = messages.map((message) => { if (message.role === 'user') { const { model, provider, content } = extractPropertiesFromMessage(message); diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index 8fdb3d7..d622b46 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -31,11 +31,14 @@ function parseCookies(cookieHeader) { async function chatAction({ context, request }: ActionFunctionArgs) { - const { messages, imageData } = await request.json<{ + const { messages, imageData, model } = await request.json<{ messages: Messages, - imageData?: string[] + imageData?: string[], + model: string }>(); + console.log('ChatAction:', JSON.stringify(messages)); + const cookieHeader = request.headers.get("Cookie"); // Parse the cookie's value (returns an object or null if no cookie exists) @@ -47,6 +50,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) { const options: StreamingOptions = { toolChoice: 'none', apiKeys, + model, onFinish: async ({ text: content, finishReason }) => { if (finishReason !== 'length') { return stream.close(); diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 308832b..501a87e 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -30,13 +30,15 @@ const PROVIDER_LIST: ProviderInfo[] = [ icon: "i-ph:cloud-arrow-down", }, { name: 'OpenAILike', - staticModels: [], + staticModels: [ + { name: 'o1-mini', label: 'o1-mini', provider: 'OpenAILike' }, + ], getDynamicModels: getOpenAILikeModels }, { name: 'OpenRouter', staticModels: [ - { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenAI' }, + { name: 'gpt-4o', label: 'GPT-4o', provider: 'OpenRouter' }, { name: 'anthropic/claude-3.5-sonnet', label: 'Anthropic: Claude 3.5 Sonnet (OpenRouter)', From df94e665d694553d5a2bacc5e9f3912f0464e67f Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Thu, 21 Nov 2024 18:09:49 -0500 Subject: [PATCH 07/12] picking right model --- app/lib/.server/llm/model.ts | 33 ++++++++++++++++++++++-------- app/lib/.server/llm/stream-text.ts | 25 +++++++++++++++++----- app/routes/api.chat.ts | 2 +- app/utils/constants.ts | 5 ++++- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/app/lib/.server/llm/model.ts b/app/lib/.server/llm/model.ts index e07f2bb..da296d0 100644 --- a/app/lib/.server/llm/model.ts +++ b/app/lib/.server/llm/model.ts @@ -15,14 +15,23 @@ export function getAnthropicModel(apiKey: string, model: string) { return anthropic(model); } -export function getOpenAILikeModel(baseURL:string,apiKey: string, model: string) { + +export function getOpenAILikeModel(baseURL: string, apiKey: string, model: string) { + // console.log('OpenAILike config:', { baseURL, hasApiKey: !!apiKey, model }); const openai = createOpenAI({ baseURL, apiKey, }); - - return openai(model); + // console.log('OpenAI client created:', !!openai); + const client = openai(model); + // console.log('OpenAI model client:', !!client); + return client; + // return { + // model: client, + // provider: 'OpenAILike' // Correctly identifying the actual provider + // }; } + export function getOpenAIModel(apiKey: string, model: string) { const openai = createOpenAI({ apiKey, @@ -74,7 +83,7 @@ export function getOllamaModel(baseURL: string, model: string) { return Ollama; } -export function getDeepseekModel(apiKey: string, model: string){ +export function getDeepseekModel(apiKey: string, model: string) { const openai = createOpenAI({ baseURL: 'https://api.deepseek.com/beta', apiKey, @@ -108,9 +117,15 @@ export function getXAIModel(apiKey: string, model: string) { return openai(model); } + export function getModel(provider: string, model: string, env: Env, apiKeys?: Record) { - const apiKey = getAPIKey(env, provider, apiKeys); - const baseURL = getBaseURL(env, provider); + let apiKey; // Declare first + let baseURL; + + apiKey = getAPIKey(env, provider, apiKeys); // Then assign + baseURL = getBaseURL(env, provider); + + // console.log('getModel inputs:', { provider, model, baseURL, hasApiKey: !!apiKey }); switch (provider) { case 'Anthropic': @@ -126,11 +141,11 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re case 'Google': return getGoogleModel(apiKey, model); case 'OpenAILike': - return getOpenAILikeModel(baseURL,apiKey, model); + return getOpenAILikeModel(baseURL, apiKey, model); case 'Deepseek': return getDeepseekModel(apiKey, model); case 'Mistral': - return getMistralModel(apiKey, model); + return getMistralModel(apiKey, model); case 'LMStudio': return getLMStudioModel(baseURL, model); case 'xAI': @@ -138,4 +153,4 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re default: return getOllamaModel(baseURL, model); } -} +} \ No newline at end of file diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 3b563ea..28c8c96 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -52,6 +52,10 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid }) : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); + // console.log('Model from message:', model); + // console.log('Found in MODEL_LIST:', MODEL_LIST.find((m) => m.name === model)); + // console.log('Current MODEL_LIST:', MODEL_LIST); + return { model, provider, content: cleanedContent }; } @@ -64,7 +68,7 @@ export function streamText( let currentModel = DEFAULT_MODEL; let currentProvider = DEFAULT_PROVIDER; - console.log('StreamText:', JSON.stringify(messages)); + // console.log('StreamText:', JSON.stringify(messages)); const processedMessages = messages.map((message) => { if (message.role === 'user') { @@ -82,11 +86,22 @@ export function streamText( return message; // No changes for non-user messages }); - return _streamText({ - model: getModel(currentProvider, currentModel, env, apiKeys), + // console.log('Message content:', messages[0].content); + // console.log('Extracted properties:', extractPropertiesFromMessage(messages[0])); + + const llmClient = getModel(currentProvider, currentModel, env, apiKeys); + // console.log('LLM Client:', llmClient); + + const llmConfig = { + ...options, + model: llmClient, //getModel(currentProvider, currentModel, env, apiKeys), + provider: currentProvider, system: getSystemPrompt(), maxTokens: MAX_TOKENS, messages: convertToCoreMessages(processedMessages), - ...options, - }); + }; + + // console.log('LLM Config:', llmConfig); + + return _streamText(llmConfig); } diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index d622b46..bc42fb2 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -37,7 +37,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) { model: string }>(); - console.log('ChatAction:', JSON.stringify(messages)); + // console.log('ChatAction:', JSON.stringify(messages)); const cookieHeader = request.headers.get("Cookie"); diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 501a87e..b1b421b 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -32,6 +32,7 @@ const PROVIDER_LIST: ProviderInfo[] = [ name: 'OpenAILike', staticModels: [ { name: 'o1-mini', label: 'o1-mini', provider: 'OpenAILike' }, + { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' }, ], getDynamicModels: getOpenAILikeModels }, @@ -58,7 +59,9 @@ const PROVIDER_LIST: ProviderInfo[] = [ }, { name: 'Google', - staticModels: [ + staticModels: [ + { name: 'gemini-exp-1121', label: 'Gemini Experimental 1121', provider: 'Google' }, + { name: 'gemini-1.5-pro-002', label: 'Gemini 1.5 Pro 002', provider: 'Google' }, { name: 'gemini-1.5-flash-latest', label: 'Gemini 1.5 Flash', provider: 'Google' }, { name: 'gemini-1.5-pro-latest', label: 'Gemini 1.5 Pro', provider: 'Google' } ], From 074161024d1bbea51a2bcb1e1b2654ca74597aac Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Thu, 21 Nov 2024 23:31:41 -0500 Subject: [PATCH 08/12] merge with upstream --- app/lib/.server/llm/model.ts | 3 --- app/lib/.server/llm/stream-text.ts | 34 ++++++++++++++---------------- app/routes/api.chat.ts | 2 -- app/utils/constants.ts | 2 +- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/app/lib/.server/llm/model.ts b/app/lib/.server/llm/model.ts index a6a560e..307c817 100644 --- a/app/lib/.server/llm/model.ts +++ b/app/lib/.server/llm/model.ts @@ -22,7 +22,6 @@ export function getAnthropicModel(apiKey: string, model: string) { } export function getOpenAILikeModel(baseURL: string, apiKey: string, model: string) { - // console.log('OpenAILike config:', { baseURL, hasApiKey: !!apiKey, model }); const openai = createOpenAI({ baseURL, apiKey, @@ -132,8 +131,6 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re apiKey = getAPIKey(env, provider, apiKeys); // Then assign baseURL = getBaseURL(env, provider); - // console.log('getModel inputs:', { provider, model, baseURL, hasApiKey: !!apiKey }); - switch (provider) { case 'Anthropic': return getAnthropicModel(apiKey, model); diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index c2e161d..965ec95 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -52,12 +52,9 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid }) : textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); - // console.log('Model from message:', model); - // console.log('Found in MODEL_LIST:', MODEL_LIST.find((m) => m.name === model)); - // console.log('Current MODEL_LIST:', MODEL_LIST); - return { model, provider, content: cleanedContent }; } + export function streamText( messages: Messages, env: Env, @@ -79,20 +76,21 @@ export function streamText( return { ...message, content }; } + return message; + }); - const modelDetails = MODEL_LIST.find((m) => m.name === currentModel); + const modelDetails = MODEL_LIST.find((m) => m.name === currentModel); - const dynamicMaxTokens = - modelDetails && modelDetails.maxTokenAllowed - ? modelDetails.maxTokenAllowed - : MAX_TOKENS; + const dynamicMaxTokens = + modelDetails && modelDetails.maxTokenAllowed + ? modelDetails.maxTokenAllowed + : MAX_TOKENS; - return _streamText({ - model: getModel(currentProvider, currentModel, env, apiKeys), - system: getSystemPrompt(), - maxTokens: dynamicMaxTokens, - messages: convertToCoreMessages(processedMessages), - ...options, - }); - } -)} + return _streamText({ + ...options, + model: getModel(currentProvider, currentModel, env, apiKeys), + system: getSystemPrompt(), + maxTokens: dynamicMaxTokens, + messages: convertToCoreMessages(processedMessages), + }); +} diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts index bc42fb2..789beb0 100644 --- a/app/routes/api.chat.ts +++ b/app/routes/api.chat.ts @@ -37,8 +37,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) { model: string }>(); - // console.log('ChatAction:', JSON.stringify(messages)); - const cookieHeader = request.headers.get("Cookie"); // Parse the cookie's value (returns an object or null if no cookie exists) diff --git a/app/utils/constants.ts b/app/utils/constants.ts index f96dceb..d781353 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -32,7 +32,7 @@ const PROVIDER_LIST: ProviderInfo[] = [ name: 'OpenAILike', staticModels: [ { name: 'o1-mini', label: 'o1-mini', provider: 'OpenAILike' }, - { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAI' }, + { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'OpenAILike' }, ], getDynamicModels: getOpenAILikeModels }, From 809b54e04aaf37e474b5151340b704d57e5afc48 Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Wed, 27 Nov 2024 14:30:09 -0500 Subject: [PATCH 09/12] upload new files --- app/components/workbench/Workbench.client.tsx | 59 ++++++++++++++ app/lib/stores/files.ts | 4 + app/lib/stores/workbench.ts | 81 +++++++++++++++++-- 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 7e21dd0..da5b60d 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -57,6 +57,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => renderLogger.trace('Workbench'); const [isSyncing, setIsSyncing] = useState(false); + const [isUploading, setIsUploading] = useState(false); const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); const showWorkbench = useStore(workbenchStore.showWorkbench); @@ -119,6 +120,60 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => } }, []); + const handleUploadFiles = useCallback(async () => { + setIsUploading(true); + + try { + // const directoryHandle = await window.showDirectoryPicker(); + + // // First upload new files + // await workbenchStore.uploadFilesFromDisk(directoryHandle); + + // // Get current files state + // const currentFiles = workbenchStore.files.get(); + + // // Create new modifications map with all files as "new" + // const newModifications = new Map(); + // Object.entries(currentFiles).forEach(([path, file]) => { + // if (file.type === 'file') { + // newModifications.set(path, file.content); + // } + // }); + + // // Update workbench state + // await workbenchStore.refreshFiles(); + // workbenchStore.resetAllFileModifications(); + + // toast.success('Files uploaded successfully'); + // } catch (error) { + // toast.error('Failed to upload files'); + // } + await handleUploadFilesFunc(); + } + + finally { + setIsUploading(false); + } + }, []); + + async function handleUploadFilesFunc() { + try { + // First clean all statuses + await workbenchStore.saveAllFiles(); + await workbenchStore.resetAllFileModifications(); + await workbenchStore.refreshFiles(); + + // Now upload new files + const directoryHandle = await window.showDirectoryPicker(); + await workbenchStore.uploadFilesFromDisk(directoryHandle); + + toast.success('Files uploaded successfully'); + } catch (error) { + console.error('Upload files error:', error); + toast.error('Failed to upload files'); + } + } + return ( chatStarted && ( {isSyncing ?
:
} {isSyncing ? 'Syncing...' : 'Sync Files'} + + {isSyncing ?
:
} + {isSyncing ? 'Uploading...' : 'Upload Files'} + { diff --git a/app/lib/stores/files.ts b/app/lib/stores/files.ts index 663ae58..b0f726e 100644 --- a/app/lib/stores/files.ts +++ b/app/lib/stores/files.ts @@ -80,6 +80,10 @@ export class FilesStore { this.#modifiedFiles.clear(); } + markFileAsNew(filePath: string) { + this.#modifiedFiles.set(filePath, ''); + } + async saveFile(filePath: string, content: string) { const webcontainer = await this.#webcontainer; diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index 4db14e7..6378ba7 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -32,6 +32,7 @@ export type WorkbenchViewType = 'code' | 'preview'; export class WorkbenchStore { #previewsStore = new PreviewsStore(webcontainer); #filesStore = new FilesStore(webcontainer); + #editorStore = new EditorStore(this.#filesStore); #terminalStore = new TerminalStore(webcontainer); @@ -43,7 +44,7 @@ export class WorkbenchStore { modifiedFiles = new Set(); artifactIdList: string[] = []; #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined; - #globalExecutionQueue=Promise.resolve(); + #globalExecutionQueue = Promise.resolve(); constructor() { if (import.meta.hot) { import.meta.hot.data.artifacts = this.artifacts; @@ -54,7 +55,7 @@ export class WorkbenchStore { } addToExecutionQueue(callback: () => Promise) { - this.#globalExecutionQueue=this.#globalExecutionQueue.then(()=>callback()) + this.#globalExecutionQueue = this.#globalExecutionQueue.then(() => callback()) } get previews() { @@ -277,11 +278,11 @@ export class WorkbenchStore { } runAction(data: ActionCallbackData, isStreaming: boolean = false) { - if(isStreaming) { + if (isStreaming) { this._runAction(data, isStreaming) } - else{ - this.addToExecutionQueue(()=>this._runAction(data, isStreaming)) + else { + this.addToExecutionQueue(() => this._runAction(data, isStreaming)) } } async _runAction(data: ActionCallbackData, isStreaming: boolean = false) { @@ -381,6 +382,61 @@ export class WorkbenchStore { return syncedFiles; } + async uploadFilesFromDisk(sourceHandle: FileSystemDirectoryHandle) { + const loadedFiles = []; + const wc = await webcontainer; + const newFiles = {}; + + const processDirectory = async (handle: FileSystemDirectoryHandle, currentPath: string = '') => { + const entries = await Array.fromAsync(handle.values()); + + for (const entry of entries) { + const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; + const fullPath = `/${entryPath}`; + + if (entry.kind === 'directory') { + await wc.fs.mkdir(fullPath, { recursive: true }); + const subDirHandle = await handle.getDirectoryHandle(entry.name); + await processDirectory(subDirHandle, entryPath); + } else { + const file = await entry.getFile(); + const content = await file.text(); + + // Write to WebContainer + await wc.fs.writeFile(fullPath, content); + + // Mark file as new + this.#filesStore.markFileAsNew(fullPath); + + // Update the files store with the current content + this.files.setKey(fullPath, { type: 'file', content, isBinary: false }); + + // Collect for editor store with actual content + newFiles[fullPath] = { type: 'file', content, isBinary: false }; + loadedFiles.push(entryPath); + } + } + } + + await processDirectory(sourceHandle); + + return loadedFiles; + } + + async refreshFiles() { + // Clear old state + this.modifiedFiles = new Set(); + this.artifactIdList = []; + + // Reset stores + this.#filesStore = new FilesStore(webcontainer); + this.#editorStore = new EditorStore(this.#filesStore); + + // Update UI state + this.currentView.set('code'); + this.unsavedFiles.set(new Set()); + } + async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) { try { @@ -486,6 +542,21 @@ export class WorkbenchStore { console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error)); } } + + async markFileAsModified(filePath: string) { + const file = this.#filesStore.getFile(filePath); + if (file?.type === 'file') { + // First collect all original content + const originalContent = file.content; + console.log(`Processing ${filePath}:`, originalContent); + + // Then save modifications + await this.saveFile(filePath, originalContent); + } + } + + + } export const workbenchStore = new WorkbenchStore(); From ccaa67b6b23dac0b67f67c67fef94fbca96482ed Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Sat, 30 Nov 2024 00:04:53 -0500 Subject: [PATCH 10/12] adjusting spaces for X button in file-preview --- app/components/chat/FilePreview.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/components/chat/FilePreview.tsx b/app/components/chat/FilePreview.tsx index 4c842ee..0500d03 100644 --- a/app/components/chat/FilePreview.tsx +++ b/app/components/chat/FilePreview.tsx @@ -1,7 +1,5 @@ -// Remove the lucide-react import import React from 'react'; -// Rest of the interface remains the same interface FilePreviewProps { files: File[]; imageDataList: string[]; @@ -14,19 +12,17 @@ const FilePreview: React.FC = ({ files, imageDataList, onRemov } return ( -
+
{files.map((file, index) => (
{imageDataList[index] && ( -
+
{file.name}
)} From 0ab334126a9520907a58383f36209cfa8208bfbd Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Mon, 2 Dec 2024 14:08:41 -0500 Subject: [PATCH 11/12] adding to display the image in the chat conversation. and paste image too. tnx to @Stijnus --- app/components/chat/BaseChat.tsx | 30 +++++++++++++++++++++++ app/components/chat/UserMessage.tsx | 38 +++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index f004b3d..5c086d4 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -190,6 +190,35 @@ export const BaseChat = React.forwardRef( input.click(); }; + const handlePaste = async (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + + if (!items) { + return; + } + + for (const item of items) { + if (item.type.startsWith('image/')) { + e.preventDefault(); + + const file = item.getAsFile(); + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + const base64Image = e.target?.result as string; + setUploadedFiles?.([...uploadedFiles, file]); + setImageDataList?.([...imageDataList, base64Image]); + }; + reader.readAsDataURL(file); + } + + break; + } + } + }; + const baseChat = (
( onChange={(event) => { handleInputChange?.(event); }} + onPaste={handlePaste} style={{ minHeight: TEXTAREA_MIN_HEIGHT, maxHeight: TEXTAREA_MAX_HEIGHT, diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index 167ce87..3e6485b 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -6,10 +6,39 @@ import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; import { Markdown } from './Markdown'; interface UserMessageProps { - content: string; + content: string | Array<{ type: string; text?: string; image?: string }>; } export function UserMessage({ content }: UserMessageProps) { + if (Array.isArray(content)) { + const textItem = content.find((item) => item.type === 'text'); + const textContent = sanitizeUserMessage(textItem?.text || ''); + const images = content.filter((item) => item.type === 'image' && item.image); + + return ( +
+
+
+ {textContent} +
+ {images.length > 0 && ( +
+ {images.map((item, index) => ( +
+ {`Uploaded +
+ ))} +
+ )} +
+
+ ); + } + const textContent = sanitizeUserMessage(content); return ( @@ -19,11 +48,6 @@ export function UserMessage({ content }: UserMessageProps) { ); } -function sanitizeUserMessage(content: string | Array<{ type: string; text?: string; image_url?: { url: string } }>) { - if (Array.isArray(content)) { - const textItem = content.find((item) => item.type === 'text'); - return textItem?.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '') || ''; - } - +function sanitizeUserMessage(content: string) { return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, ''); } From 5adc0f681c60b15e69da97dadfccf8efd3707259 Mon Sep 17 00:00:00 2001 From: Andrew Trokhymenko Date: Mon, 2 Dec 2024 20:27:10 -0500 Subject: [PATCH 12/12] adding drag and drop images to text area --- app/components/chat/BaseChat.tsx | 38 +++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 233aa66..749af6c 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -351,9 +351,41 @@ export const BaseChat = React.forwardRef( >