Refactor to use newver v4 version of Vercel AI package

This commit is contained in:
eduardruzga 2024-12-09 17:26:33 +02:00
parent 6e61a4fb95
commit fcb61ba499
9 changed files with 441 additions and 608 deletions

View File

@ -112,13 +112,22 @@ export const ChatImpl = memo(
body: { body: {
apiKeys, apiKeys,
}, },
sendExtraMessageFields: true,
onError: (error) => { onError: (error) => {
logger.error('Request failed\n\n', error); logger.error('Request failed\n\n', error);
toast.error( toast.error(
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'), 'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
); );
}, },
onFinish: () => { onFinish: (message, response) => {
const usage = response.usage;
if (usage) {
console.log('Token usage:', usage);
// You can now use the usage data as needed
}
logger.debug('Finished streaming'); logger.debug('Finished streaming');
}, },
initialMessages, initialMessages,

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import type { Message } from 'ai'; import type { Message } from 'ai';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils'; import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
import { createChatFromFolder } from '../../utils/folderImport'; import { createChatFromFolder } from '~/utils/folderImport';
interface ImportFolderButtonProps { interface ImportFolderButtonProps {
className?: string; className?: string;
@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
if (allFiles.length > MAX_FILES) { if (allFiles.length > MAX_FILES) {
toast.error( toast.error(
`This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.` `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
); );
return; return;
} }
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder'; const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
setIsLoading(true); setIsLoading(true);
const loadingToast = toast.loading(`Importing ${folderName}...`); const loadingToast = toast.loading(`Importing ${folderName}...`);
try { try {

View File

@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
disabled={disabled} disabled={disabled}
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
if (!disabled) { if (!disabled) {
onClick?.(event); onClick?.(event);
} }

View File

@ -8,17 +8,15 @@ export async function action(args: ActionFunctionArgs) {
return chatAction(args); return chatAction(args);
} }
function parseCookies(cookieHeader: string) { function parseCookies(cookieHeader: string): Record<string, string> {
const cookies: any = {}; const cookies: Record<string, string> = {};
// Split the cookie string by semicolons and spaces
const items = cookieHeader.split(';').map((cookie) => cookie.trim()); const items = cookieHeader.split(';').map((cookie) => cookie.trim());
items.forEach((item) => { items.forEach((item) => {
const [name, ...rest] = item.split('='); const [name, ...rest] = item.split('=');
if (name && rest) { if (name && rest) {
// Decode the name and value, and join value parts in case it contains '='
const decodedName = decodeURIComponent(name.trim()); const decodedName = decodeURIComponent(name.trim());
const decodedValue = decodeURIComponent(rest.join('=').trim()); const decodedValue = decodeURIComponent(rest.join('=').trim());
cookies[decodedName] = decodedValue; cookies[decodedName] = decodedValue;
@ -35,16 +33,15 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}>(); }>();
const cookieHeader = request.headers.get('Cookie'); 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 apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}');
const stream = new SwitchableStream(); const stream = new SwitchableStream();
try { try {
const options: StreamingOptions = { const options: StreamingOptions = {
toolChoice: 'none', toolChoice: 'none',
onFinish: async ({ text: content, finishReason }) => { onFinish: async ({ text: content, finishReason, usage }) => {
console.log('usage', usage);
if (finishReason !== 'length') { if (finishReason !== 'length') {
return stream.close(); return stream.close();
} }
@ -62,13 +59,12 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const result = await streamText(messages, context.cloudflare.env, options, apiKeys); const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
return stream.switchSource(result.toAIStream()); return stream.switchSource(result.toDataStream());
}, },
}; };
const result = await streamText(messages, context.cloudflare.env, options, apiKeys); const result = await streamText(messages, context.cloudflare.env, options, apiKeys);
stream.switchSource(result.toDataStream());
stream.switchSource(result.toAIStream());
return new Response(stream.readable, { return new Response(stream.readable, {
status: 200, status: 200,
@ -77,7 +73,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}, },
}); });
} catch (error: any) { } catch (error: any) {
console.log(error); console.error(error);
if (error.message?.includes('API key')) { if (error.message?.includes('API key')) {
throw new Response('Invalid or missing API key', { throw new Response('Invalid or missing API key', {

View File

@ -1,5 +1,6 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare'; import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { StreamingTextResponse, parseStreamPart } from 'ai';
//import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text'; import { streamText } from '~/lib/.server/llm/stream-text';
import { stripIndents } from '~/utils/stripIndent'; import { stripIndents } from '~/utils/stripIndent';
import type { ProviderInfo } from '~/types/model'; import type { ProviderInfo } from '~/types/model';
@ -58,7 +59,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
- Use professional language - Use professional language
For invalid or unclear prompts: For invalid or unclear prompts:
- Respond with a clear, professional guidance message - Respond with clear, professional guidance
- Keep responses concise and actionable - Keep responses concise and actionable
- Maintain a helpful, constructive tone - Maintain a helpful, constructive tone
- Focus on what the user should provide - Focus on what the user should provide
@ -85,7 +86,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
for (const line of lines) { for (const line of lines) {
try { try {
const parsed = parseStreamPart(line); const parsed = JSON.parse(line);
if (parsed.type === 'text') { if (parsed.type === 'text') {
controller.enqueue(encoder.encode(parsed.value)); controller.enqueue(encoder.encode(parsed.value));
@ -100,7 +101,12 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
const transformedStream = result.toDataStream().pipeThrough(transformStream); const transformedStream = result.toDataStream().pipeThrough(transformStream);
return new StreamingTextResponse(transformedStream); return new Response(transformedStream, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error: unknown) { } catch (error: unknown) {
console.log(error); console.log(error);

View File

@ -29,10 +29,12 @@ export const isBinaryFile = async (file: File): Promise<boolean> => {
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
const byte = buffer[i]; const byte = buffer[i];
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) { if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
return true; return true;
} }
} }
return false; return false;
}; };
@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => {
}; };
const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => { const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json')); const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
if (!packageJsonFile) return null;
if (!packageJsonFile) {
return null;
}
try { try {
const content = await new Promise<string>((resolve, reject) => { const content = await new Promise<string>((resolve, reject) => {
@ -59,8 +64,10 @@ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string
} }
}; };
export const detectProjectType = async (files: File[]): Promise<{ type: string; setupCommand: string; followupMessage: string }> => { export const detectProjectType = async (
const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name)); files: File[],
): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
if (hasFile('package.json')) { if (hasFile('package.json')) {
const packageJson = await readPackageJson(files); const packageJson = await readPackageJson(files);
@ -68,20 +75,21 @@ export const detectProjectType = async (files: File[]): Promise<{ type: string;
// Check for preferred commands in priority order // Check for preferred commands in priority order
const preferredCommands = ['dev', 'start', 'preview']; const preferredCommands = ['dev', 'start', 'preview'];
const availableCommand = preferredCommands.find(cmd => scripts[cmd]); const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
if (availableCommand) { if (availableCommand) {
return { return {
type: 'Node.js', type: 'Node.js',
setupCommand: `npm install && npm run ${availableCommand}`, setupCommand: `npm install && npm run ${availableCommand}`,
followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.` followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
}; };
} }
return { return {
type: 'Node.js', type: 'Node.js',
setupCommand: 'npm install', setupCommand: 'npm install',
followupMessage: 'Would you like me to inspect package.json to determine the available scripts for running this project?' followupMessage:
'Would you like me to inspect package.json to determine the available scripts for running this project?',
}; };
} }
@ -89,7 +97,7 @@ export const detectProjectType = async (files: File[]): Promise<{ type: string;
return { return {
type: 'Static', type: 'Static',
setupCommand: 'npx --yes serve', setupCommand: 'npx --yes serve',
followupMessage: '' followupMessage: '',
}; };
} }

View File

@ -4,12 +4,13 @@ import { generateId, detectProjectType } from './fileUtils';
export const createChatFromFolder = async ( export const createChatFromFolder = async (
files: File[], files: File[],
binaryFiles: string[], binaryFiles: string[],
folderName: string folderName: string,
): Promise<Message[]> => { ): Promise<Message[]> => {
const fileArtifacts = await Promise.all( const fileArtifacts = await Promise.all(
files.map(async (file) => { files.map(async (file) => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const content = reader.result as string; const content = reader.result as string;
const relativePath = file.webkitRelativePath.split('/').slice(1).join('/'); const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
@ -26,14 +27,18 @@ ${content}
); );
const project = await detectProjectType(files); const project = await detectProjectType(files);
const setupCommand = project.setupCommand ? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>` : ''; const setupCommand = project.setupCommand
? `\n\n<boltAction type="shell">\n${project.setupCommand}\n</boltAction>`
: '';
const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : ''; const followupMessage = project.followupMessage ? `\n\n${project.followupMessage}` : '';
const binaryFilesMessage = binaryFiles.length > 0 const binaryFilesMessage =
binaryFiles.length > 0
? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}` ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
: ''; : '';
const assistantMessages: Message[] = [{ const assistantMessages: Message[] = [
{
role: 'assistant', role: 'assistant',
content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage} content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
@ -42,7 +47,8 @@ ${fileArtifacts.join('\n\n')}
</boltArtifact>`, </boltArtifact>`,
id: generateId(), id: generateId(),
createdAt: new Date(), createdAt: new Date(),
},{ },
{
role: 'assistant', role: 'assistant',
content: ` content: `
<boltArtifact id="imported-files" title="Imported Files"> <boltArtifact id="imported-files" title="Imported Files">
@ -50,7 +56,8 @@ ${setupCommand}
</boltArtifact>${followupMessage}`, </boltArtifact>${followupMessage}`,
id: generateId(), id: generateId(),
createdAt: new Date(), createdAt: new Date(),
}]; },
];
const userMessage: Message = { const userMessage: Message = {
role: 'user', role: 'user',

View File

@ -69,7 +69,7 @@
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"ai": "^3.4.33", "ai": "^4.0.13",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"diff": "^5.2.0", "diff": "^5.2.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",

File diff suppressed because it is too large Load Diff