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: {
apiKeys,
},
sendExtraMessageFields: true,
onError: (error) => {
logger.error('Request failed\n\n', error);
toast.error(
'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');
},
initialMessages,

View File

@ -1,8 +1,8 @@
import React, { useState } from 'react';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '../../utils/fileUtils';
import { createChatFromFolder } from '../../utils/folderImport';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
import { createChatFromFolder } from '~/utils/folderImport';
interface ImportFolderButtonProps {
className?: string;
@ -17,12 +17,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
if (allFiles.length > MAX_FILES) {
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;
}
const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
setIsLoading(true);
const loadingToast = toast.loading(`Importing ${folderName}...`);
try {

View File

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

View File

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

View File

@ -1,5 +1,6 @@
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 { stripIndents } from '~/utils/stripIndent';
import type { ProviderInfo } from '~/types/model';
@ -58,7 +59,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
- Use professional language
For invalid or unclear prompts:
- Respond with a clear, professional guidance message
- Respond with clear, professional guidance
- Keep responses concise and actionable
- Maintain a helpful, constructive tone
- Focus on what the user should provide
@ -85,7 +86,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
for (const line of lines) {
try {
const parsed = parseStreamPart(line);
const parsed = JSON.parse(line);
if (parsed.type === 'text') {
controller.enqueue(encoder.encode(parsed.value));
@ -100,7 +101,12 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
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) {
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++) {
const byte = buffer[i];
if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
return true;
}
}
return false;
};
@ -41,8 +43,11 @@ export const shouldIncludeFile = (path: string): boolean => {
};
const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
const packageJsonFile = files.find(f => f.webkitRelativePath.endsWith('package.json'));
if (!packageJsonFile) return null;
const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
if (!packageJsonFile) {
return null;
}
try {
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 }> => {
const hasFile = (name: string) => files.some(f => f.webkitRelativePath.endsWith(name));
export const detectProjectType = async (
files: File[],
): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
if (hasFile('package.json')) {
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
const preferredCommands = ['dev', 'start', 'preview'];
const availableCommand = preferredCommands.find(cmd => scripts[cmd]);
const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
if (availableCommand) {
return {
type: 'Node.js',
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 {
type: 'Node.js',
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 {
type: 'Static',
setupCommand: 'npx --yes serve',
followupMessage: ''
followupMessage: '',
};
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff