changing based on PR review
Browse files- app/components/chat/BaseChat.tsx +35 -35
- app/components/chat/FilePreview.tsx +6 -7
- app/components/chat/UserMessage.tsx +0 -3
- app/lib/.server/llm/stream-text.ts +2 -12
- app/routes/api.chat.ts +3 -10
- package.json +0 -1
- pnpm-lock.yaml +0 -12
app/components/chat/BaseChat.tsx
CHANGED
|
@@ -35,7 +35,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
|
|
| 35 |
<select
|
| 36 |
value={provider?.name}
|
| 37 |
onChange={(e) => {
|
| 38 |
-
setProvider(providerList.find(p => p.name === e.target.value));
|
| 39 |
const firstModel = [...modelList].find((m) => m.provider == e.target.value);
|
| 40 |
setModel(firstModel ? firstModel.name : '');
|
| 41 |
}}
|
|
@@ -51,7 +51,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
|
|
| 51 |
key={provider?.name}
|
| 52 |
value={model}
|
| 53 |
onChange={(e) => setModel(e.target.value)}
|
| 54 |
-
style={{ maxWidth:
|
| 55 |
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"
|
| 56 |
>
|
| 57 |
{[...modelList]
|
|
@@ -93,32 +93,34 @@ interface BaseChatProps {
|
|
| 93 |
setImageDataList?: (dataList: string[]) => void;
|
| 94 |
}
|
| 95 |
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
| 96 |
-
(
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
| 122 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
| 123 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
| 124 |
const [modelList, setModelList] = useState(MODEL_LIST);
|
|
@@ -139,7 +141,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
| 139 |
Cookies.remove('apiKeys');
|
| 140 |
}
|
| 141 |
|
| 142 |
-
initializeModelList().then(modelList => {
|
| 143 |
setModelList(modelList);
|
| 144 |
});
|
| 145 |
}, []);
|
|
@@ -239,12 +241,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
| 239 |
setProvider={setProvider}
|
| 240 |
providerList={PROVIDER_LIST}
|
| 241 |
/>
|
| 242 |
-
{provider &&
|
| 243 |
<APIKeyManager
|
| 244 |
provider={provider}
|
| 245 |
apiKey={apiKeys[provider.name] || ''}
|
| 246 |
setApiKey={(key) => updateApiKey(provider.name, key)}
|
| 247 |
-
/>
|
|
|
|
| 248 |
|
| 249 |
<FilePreview
|
| 250 |
files={uploadedFiles}
|
|
@@ -309,7 +312,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
| 309 |
className="transition-all"
|
| 310 |
onClick={() => handleFileUpload()}
|
| 311 |
>
|
| 312 |
-
<div className="i-ph:
|
| 313 |
</IconButton>
|
| 314 |
|
| 315 |
<IconButton
|
|
@@ -374,6 +377,3 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|
| 374 |
);
|
| 375 |
},
|
| 376 |
);
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
| 35 |
<select
|
| 36 |
value={provider?.name}
|
| 37 |
onChange={(e) => {
|
| 38 |
+
setProvider(providerList.find((p) => p.name === e.target.value));
|
| 39 |
const firstModel = [...modelList].find((m) => m.provider == e.target.value);
|
| 40 |
setModel(firstModel ? firstModel.name : '');
|
| 41 |
}}
|
|
|
|
| 51 |
key={provider?.name}
|
| 52 |
value={model}
|
| 53 |
onChange={(e) => setModel(e.target.value)}
|
| 54 |
+
style={{ maxWidth: '70%' }}
|
| 55 |
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"
|
| 56 |
>
|
| 57 |
{[...modelList]
|
|
|
|
| 93 |
setImageDataList?: (dataList: string[]) => void;
|
| 94 |
}
|
| 95 |
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
| 96 |
+
(
|
| 97 |
+
{
|
| 98 |
+
textareaRef,
|
| 99 |
+
messageRef,
|
| 100 |
+
scrollRef,
|
| 101 |
+
showChat = true,
|
| 102 |
+
chatStarted = false,
|
| 103 |
+
isStreaming = false,
|
| 104 |
+
model,
|
| 105 |
+
setModel,
|
| 106 |
+
provider,
|
| 107 |
+
setProvider,
|
| 108 |
+
input = '',
|
| 109 |
+
enhancingPrompt,
|
| 110 |
+
handleInputChange,
|
| 111 |
+
promptEnhanced,
|
| 112 |
+
enhancePrompt,
|
| 113 |
+
sendMessage,
|
| 114 |
+
handleStop,
|
| 115 |
+
uploadedFiles,
|
| 116 |
+
setUploadedFiles,
|
| 117 |
+
imageDataList,
|
| 118 |
+
setImageDataList,
|
| 119 |
+
messages,
|
| 120 |
+
children, // Add this
|
| 121 |
+
},
|
| 122 |
+
ref,
|
| 123 |
+
) => {
|
| 124 |
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
| 125 |
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
| 126 |
const [modelList, setModelList] = useState(MODEL_LIST);
|
|
|
|
| 141 |
Cookies.remove('apiKeys');
|
| 142 |
}
|
| 143 |
|
| 144 |
+
initializeModelList().then((modelList) => {
|
| 145 |
setModelList(modelList);
|
| 146 |
});
|
| 147 |
}, []);
|
|
|
|
| 241 |
setProvider={setProvider}
|
| 242 |
providerList={PROVIDER_LIST}
|
| 243 |
/>
|
| 244 |
+
{provider && (
|
| 245 |
<APIKeyManager
|
| 246 |
provider={provider}
|
| 247 |
apiKey={apiKeys[provider.name] || ''}
|
| 248 |
setApiKey={(key) => updateApiKey(provider.name, key)}
|
| 249 |
+
/>
|
| 250 |
+
)}
|
| 251 |
|
| 252 |
<FilePreview
|
| 253 |
files={uploadedFiles}
|
|
|
|
| 312 |
className="transition-all"
|
| 313 |
onClick={() => handleFileUpload()}
|
| 314 |
>
|
| 315 |
+
<div className="i-ph:paperclip text-xl"></div>
|
| 316 |
</IconButton>
|
| 317 |
|
| 318 |
<IconButton
|
|
|
|
| 377 |
);
|
| 378 |
},
|
| 379 |
);
|
|
|
|
|
|
|
|
|
app/components/chat/FilePreview.tsx
CHANGED
|
@@ -1,23 +1,22 @@
|
|
| 1 |
-
//
|
| 2 |
import React from 'react';
|
| 3 |
-
import { X } from 'lucide-react';
|
| 4 |
|
|
|
|
| 5 |
interface FilePreviewProps {
|
| 6 |
files: File[];
|
| 7 |
-
imageDataList: string[];
|
| 8 |
onRemove: (index: number) => void;
|
| 9 |
}
|
| 10 |
|
| 11 |
const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
|
| 12 |
if (!files || files.length === 0) {
|
| 13 |
-
return null;
|
| 14 |
}
|
| 15 |
|
| 16 |
return (
|
| 17 |
-
<div className="flex flex-row overflow-x-auto">
|
| 18 |
{files.map((file, index) => (
|
| 19 |
<div key={file.name + file.size} className="mr-2 relative">
|
| 20 |
-
{/* Display image preview or file icon */}
|
| 21 |
{imageDataList[index] && (
|
| 22 |
<div className="relative">
|
| 23 |
<img src={imageDataList[index]} alt={file.name} className="max-h-20" />
|
|
@@ -26,7 +25,7 @@ const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemov
|
|
| 26 |
className="absolute -top-2 -right-2 z-10 bg-white rounded-full p-1 shadow-md hover:bg-gray-100"
|
| 27 |
>
|
| 28 |
<div className="bg-black rounded-full p-1">
|
| 29 |
-
<
|
| 30 |
</div>
|
| 31 |
</button>
|
| 32 |
</div>
|
|
|
|
| 1 |
+
// Remove the lucide-react import
|
| 2 |
import React from 'react';
|
|
|
|
| 3 |
|
| 4 |
+
// Rest of the interface remains the same
|
| 5 |
interface FilePreviewProps {
|
| 6 |
files: File[];
|
| 7 |
+
imageDataList: string[];
|
| 8 |
onRemove: (index: number) => void;
|
| 9 |
}
|
| 10 |
|
| 11 |
const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
|
| 12 |
if (!files || files.length === 0) {
|
| 13 |
+
return null;
|
| 14 |
}
|
| 15 |
|
| 16 |
return (
|
| 17 |
+
<div className="flex flex-row overflow-x-auto">
|
| 18 |
{files.map((file, index) => (
|
| 19 |
<div key={file.name + file.size} className="mr-2 relative">
|
|
|
|
| 20 |
{imageDataList[index] && (
|
| 21 |
<div className="relative">
|
| 22 |
<img src={imageDataList[index]} alt={file.name} className="max-h-20" />
|
|
|
|
| 25 |
className="absolute -top-2 -right-2 z-10 bg-white rounded-full p-1 shadow-md hover:bg-gray-100"
|
| 26 |
>
|
| 27 |
<div className="bg-black rounded-full p-1">
|
| 28 |
+
<div className="i-ph:x w-3 h-3 text-gray-400" />
|
| 29 |
</div>
|
| 30 |
</button>
|
| 31 |
</div>
|
app/components/chat/UserMessage.tsx
CHANGED
|
@@ -21,9 +21,6 @@ export function UserMessage({ content }: UserMessageProps) {
|
|
| 21 |
);
|
| 22 |
}
|
| 23 |
|
| 24 |
-
// function sanitizeUserMessage(content: string) {
|
| 25 |
-
// return content.replace(modificationsRegex, '').replace(MODEL_REGEX, 'Using: $1').replace(PROVIDER_REGEX, ' ($1)\n\n').trim();
|
| 26 |
-
// }
|
| 27 |
function sanitizeUserMessage(content: string | Array<{type: string, text?: string, image_url?: {url: string}}>) {
|
| 28 |
if (Array.isArray(content)) {
|
| 29 |
return content.map(item => {
|
|
|
|
| 21 |
);
|
| 22 |
}
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
function sanitizeUserMessage(content: string | Array<{type: string, text?: string, image_url?: {url: string}}>) {
|
| 25 |
if (Array.isArray(content)) {
|
| 26 |
return content.map(item => {
|
app/lib/.server/llm/stream-text.ts
CHANGED
|
@@ -45,12 +45,12 @@ function extractPropertiesFromMessage(message: Message): { model: string; provid
|
|
| 45 |
if (item.type === 'text') {
|
| 46 |
return {
|
| 47 |
type: 'text',
|
| 48 |
-
text: item.text?.replace(
|
| 49 |
};
|
| 50 |
}
|
| 51 |
return item; // Preserve image_url and other types as is
|
| 52 |
})
|
| 53 |
-
: textContent.replace(
|
| 54 |
|
| 55 |
return { model, provider, content: cleanedContent };
|
| 56 |
}
|
|
@@ -80,16 +80,6 @@ export function streamText(
|
|
| 80 |
return message; // No changes for non-user messages
|
| 81 |
});
|
| 82 |
|
| 83 |
-
// const modelConfig = getModel(currentProvider, currentModel, env, apiKeys);
|
| 84 |
-
// const coreMessages = convertToCoreMessages(processedMessages);
|
| 85 |
-
|
| 86 |
-
// console.log('Debug streamText:', JSON.stringify({
|
| 87 |
-
// model: modelConfig,
|
| 88 |
-
// messages: processedMessages,
|
| 89 |
-
// coreMessages: coreMessages,
|
| 90 |
-
// system: getSystemPrompt()
|
| 91 |
-
// }, null, 2));
|
| 92 |
-
|
| 93 |
return _streamText({
|
| 94 |
model: getModel(currentProvider, currentModel, env, apiKeys),
|
| 95 |
system: getSystemPrompt(),
|
|
|
|
| 45 |
if (item.type === 'text') {
|
| 46 |
return {
|
| 47 |
type: 'text',
|
| 48 |
+
text: item.text?.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '')
|
| 49 |
};
|
| 50 |
}
|
| 51 |
return item; // Preserve image_url and other types as is
|
| 52 |
})
|
| 53 |
+
: textContent.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
| 54 |
|
| 55 |
return { model, provider, content: cleanedContent };
|
| 56 |
}
|
|
|
|
| 80 |
return message; // No changes for non-user messages
|
| 81 |
});
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
return _streamText({
|
| 84 |
model: getModel(currentProvider, currentModel, env, apiKeys),
|
| 85 |
system: getSystemPrompt(),
|
app/routes/api.chat.ts
CHANGED
|
@@ -30,15 +30,15 @@ function parseCookies(cookieHeader) {
|
|
| 30 |
}
|
| 31 |
|
| 32 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
| 33 |
-
|
| 34 |
-
// console.log('Request received:', request.url);
|
| 35 |
-
|
| 36 |
const { messages, imageData } = await request.json<{
|
| 37 |
messages: Messages,
|
| 38 |
imageData?: string[]
|
| 39 |
}>();
|
| 40 |
|
| 41 |
const cookieHeader = request.headers.get("Cookie");
|
|
|
|
|
|
|
| 42 |
const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || "{}");
|
| 43 |
|
| 44 |
const stream = new SwitchableStream();
|
|
@@ -71,13 +71,6 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
|
| 71 |
|
| 72 |
const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
|
| 73 |
|
| 74 |
-
// console.log('=== API CHAT LOGGING START ===');
|
| 75 |
-
// console.log('StreamText:', JSON.stringify({
|
| 76 |
-
// messages,
|
| 77 |
-
// result,
|
| 78 |
-
// }, null, 2));
|
| 79 |
-
// console.log('=== API CHAT LOGGING END ===');
|
| 80 |
-
|
| 81 |
stream.switchSource(result.toAIStream());
|
| 82 |
|
| 83 |
return new Response(stream.readable, {
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
async function chatAction({ context, request }: ActionFunctionArgs) {
|
| 33 |
+
|
|
|
|
|
|
|
| 34 |
const { messages, imageData } = await request.json<{
|
| 35 |
messages: Messages,
|
| 36 |
imageData?: string[]
|
| 37 |
}>();
|
| 38 |
|
| 39 |
const cookieHeader = request.headers.get("Cookie");
|
| 40 |
+
|
| 41 |
+
// Parse the cookie's value (returns an object or null if no cookie exists)
|
| 42 |
const apiKeys = JSON.parse(parseCookies(cookieHeader).apiKeys || "{}");
|
| 43 |
|
| 44 |
const stream = new SwitchableStream();
|
|
|
|
| 71 |
|
| 72 |
const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
stream.switchSource(result.toAIStream());
|
| 75 |
|
| 76 |
return new Response(stream.readable, {
|
package.json
CHANGED
|
@@ -74,7 +74,6 @@
|
|
| 74 |
"jose": "^5.6.3",
|
| 75 |
"js-cookie": "^3.0.5",
|
| 76 |
"jszip": "^3.10.1",
|
| 77 |
-
"lucide-react": "^0.460.0",
|
| 78 |
"nanostores": "^0.10.3",
|
| 79 |
"ollama-ai-provider": "^0.15.2",
|
| 80 |
"react": "^18.2.0",
|
|
|
|
| 74 |
"jose": "^5.6.3",
|
| 75 |
"js-cookie": "^3.0.5",
|
| 76 |
"jszip": "^3.10.1",
|
|
|
|
| 77 |
"nanostores": "^0.10.3",
|
| 78 |
"ollama-ai-provider": "^0.15.2",
|
| 79 |
"react": "^18.2.0",
|
pnpm-lock.yaml
CHANGED
|
@@ -155,9 +155,6 @@ importers:
|
|
| 155 |
jszip:
|
| 156 |
specifier: ^3.10.1
|
| 157 |
version: 3.10.1
|
| 158 |
-
lucide-react:
|
| 159 |
-
specifier: ^0.460.0
|
| 160 |
-
version: 0.460.0([email protected])
|
| 161 |
nanostores:
|
| 162 |
specifier: ^0.10.3
|
| 163 |
version: 0.10.3
|
|
@@ -3674,11 +3671,6 @@ packages:
|
|
| 3674 |
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
| 3675 |
engines: {node: '>=12'}
|
| 3676 |
|
| 3677 | |
| 3678 |
-
resolution: {integrity: sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==}
|
| 3679 |
-
peerDependencies:
|
| 3680 |
-
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
| 3681 |
-
|
| 3682 | |
| 3683 |
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
| 3684 |
|
|
@@ -9492,10 +9484,6 @@ snapshots:
|
|
| 9492 |
|
| 9493 | |
| 9494 |
|
| 9495 | |
| 9496 |
-
dependencies:
|
| 9497 |
-
react: 18.3.1
|
| 9498 |
-
|
| 9499 | |
| 9500 |
dependencies:
|
| 9501 |
sourcemap-codec: 1.4.8
|
|
|
|
| 155 |
jszip:
|
| 156 |
specifier: ^3.10.1
|
| 157 |
version: 3.10.1
|
|
|
|
|
|
|
|
|
|
| 158 |
nanostores:
|
| 159 |
specifier: ^0.10.3
|
| 160 |
version: 0.10.3
|
|
|
|
| 3671 |
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
| 3672 |
engines: {node: '>=12'}
|
| 3673 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3674 | |
| 3675 |
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
| 3676 |
|
|
|
|
| 9484 |
|
| 9485 | |
| 9486 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9487 | |
| 9488 |
dependencies:
|
| 9489 |
sourcemap-codec: 1.4.8
|