Add Together AI model support

This commit is contained in:
KevIsDev 2024-10-10 01:27:53 +01:00 committed by KevIsDev
parent cecbc55380
commit a93e91c433
23 changed files with 25632 additions and 87 deletions

View File

@ -62,6 +62,10 @@ pnpm install
ANTHROPIC_API_KEY=XXX
```
```
TOGETHER_API_KEY=XXX
```
Optionally, you can set the debug level:
```
@ -70,9 +74,21 @@ VITE_LOG_LEVEL=debug
**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore.
## Add Custom Models from Together AI
To add custom models from Together AI, you can add them to the `app/components/chat/ProviderSelector.tsx` file.
```
const togetherModels = [
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
'mistralai/Mixtral-8x7B-Instruct-v0.1',
'... add more models here ...'
];
## Available Scripts
- `pnpm run dev`: Starts the development server.
- `pnpm run dev`: Starts the development server (use Chrome Canary for best results when testing locally).
- `pnpm run build`: Builds the project.
- `pnpm run start`: Runs the built application locally using Wrangler Pages. This script uses `bindings.sh` to set up necessary bindings so you don't have to duplicate environment variables.
- `pnpm run preview`: Builds the project and then starts it locally, useful for testing the production build. Note, HTTP streaming currently doesn't work as expected with `wrangler pages dev`.

View File

@ -60,7 +60,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
>
<div className="px-5 p-3.5 w-full text-left">
<div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div>
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div>
<div className="w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div>
</div>
</button>
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />

View File

@ -12,6 +12,8 @@ import { fileModificationsToHTML } from '~/utils/diff';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import { ProviderSelector } from './ProviderSelector';
import { providerStore } from '~/lib/stores/provider';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@ -75,6 +77,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
const [animationScope, animate] = useAnimate();
const provider = useStore(providerStore);
const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
api: '/api/chat',
onError: (error) => {
@ -85,6 +89,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
logger.debug('Finished streaming');
},
initialMessages,
body: {
provider: provider === 'anthropic' ? 'anthropic' : { type: 'together', model: provider.model },
},
});
const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
@ -153,13 +160,6 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
return;
}
/**
* @note (delm) Usually saving files shouldn't take long but it may take longer if there
* many unsaved files. In that case we need to block user input and show an indicator
* of some kind so the user is aware that something is happening. But I consider the
* happy case to be no unsaved files and I would expect users to save their changes
* before they send another message.
*/
await workbenchStore.saveAllFiles();
const fileModifications = workbenchStore.getFileModifcations();
@ -171,64 +171,59 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
if (fileModifications !== undefined) {
const diff = fileModificationsToHTML(fileModifications);
/**
* If we have file modifications we append a new user message manually since we have to prefix
* the user input with the file modifications and we don't want the new user input to appear
* in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
* manually reset the input and we'd have to manually pass in file attachments. However, those
* aren't relevant here.
*/
append({ role: 'user', content: `${diff}\n\n${_input}` });
append({
role: 'user',
content: `${diff}\n\n${_input}`,
// We don't need to manually include the provider here as it's handled by the useChat hook
});
/**
* After sending a new message we reset all modifications since the model
* should now be aware of all the changes.
*/
workbenchStore.resetAllFileModifications();
} else {
append({ role: 'user', content: _input });
append({
role: 'user',
content: _input,
// We don't need to manually include the provider here as it's handled by the useChat hook
});
}
setInput('');
resetEnhancer();
textareaRef.current?.blur();
};
const [messageRef, scrollRef] = useSnapScroll();
return (
<BaseChat
ref={animationScope}
textareaRef={textareaRef}
input={input}
showChat={showChat}
chatStarted={chatStarted}
isStreaming={isLoading}
enhancingPrompt={enhancingPrompt}
promptEnhanced={promptEnhanced}
sendMessage={sendMessage}
messageRef={messageRef}
scrollRef={scrollRef}
handleInputChange={handleInputChange}
handleStop={abort}
messages={messages.map((message, i) => {
if (message.role === 'user') {
return message;
}
<BaseChat
ref={animationScope}
textareaRef={textareaRef}
input={input}
showChat={showChat}
chatStarted={chatStarted}
isStreaming={isLoading}
enhancingPrompt={enhancingPrompt}
promptEnhanced={promptEnhanced}
sendMessage={sendMessage}
messageRef={messageRef}
scrollRef={scrollRef}
handleInputChange={handleInputChange}
handleStop={abort}
messages={messages.map((message, i) => {
if (message.role === 'user') {
return message;
}
return {
...message,
content: parsedMessages[i] || '',
};
})}
enhancePrompt={() => {
enhancePrompt(input, (input) => {
setInput(input);
scrollTextArea();
});
}}
/>
return {
...message,
content: parsedMessages[i] || '',
};
})}
enhancePrompt={() => {
enhancePrompt(input, (input) => {
setInput(input);
scrollTextArea();
});
}}
/>
);
});
});

View File

@ -0,0 +1,60 @@
import React from 'react';
import { useStore } from '@nanostores/react';
import { providerStore, setProvider, type Provider } from '~/lib/stores/provider';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from '~/components/ui/dropdown-menu';
import { Button } from '~/components/ui/button';
import '~/styles/index.scss';
import { ChevronDownIcon } from '@radix-ui/react-icons';
export function ProviderSelector() {
const currentProvider = useStore(providerStore);
const handleProviderChange = (value: string) => {
if (value === 'anthropic') {
setProvider('anthropic');
} else {
setProvider({ type: 'together', model: value });
}
};
const togetherModels = [
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
'mistralai/Mixtral-8x7B-Instruct-v0.1',
// Add more Together AI models here
];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-[250px] justify-between bg-transparent border-none ring-transparent outline-transparent text-white hover:text-white/80 truncate">
{currentProvider === 'anthropic'
? 'Anthropic (Claude)'
: `Together AI (${currentProvider.model})`}
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[250px] bg-black text-white border-none">
<DropdownMenuRadioGroup
value={currentProvider === 'anthropic' ? 'anthropic' : currentProvider.model}
onValueChange={handleProviderChange}
>
<DropdownMenuRadioItem value="anthropic" className="hover:bg-white/10">
Anthropic (Claude)
</DropdownMenuRadioItem>
{togetherModels.map(model => (
<DropdownMenuRadioItem key={model} value={model} className="hover:bg-white/10">
Together AI ({model})
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -4,6 +4,7 @@ import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
import { ProviderSelector } from '../chat/ProviderSelector';
export function Header() {
const chat = useStore(chatStore);
@ -11,7 +12,7 @@ export function Header() {
return (
<header
className={classNames(
'flex items-center bg-bolt-elements-background-depth-1 p-5 border-b h-[var(--header-height)]',
'flex items-center justify-between bg-bolt-elements-background-depth-1 p-5 border-b h-[var(--header-height)]',
{
'border-transparent': !chat.started,
'border-bolt-elements-borderColor': chat.started,
@ -27,15 +28,18 @@ export function Header() {
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
{chat.started && (
<ClientOnly>
{() => (
<div className="mr-1">
<HeaderActionButtons />
</div>
)}
</ClientOnly>
)}
<div className="flex items-center gap-4">
<ClientOnly>{() => <ProviderSelector />}</ClientOnly>
{chat.started && (
<ClientOnly>
{() => (
<div className="mr-1">
<HeaderActionButtons />
</div>
)}
</ClientOnly>
)}
</div>
</header>
);
}

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "~/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

81
app/index.css Normal file
View File

@ -0,0 +1,81 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--card: 0 0% 100%;
--card-foreground: 222.2 47.4% 11.2%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 100% 50%;
--destructive-foreground: 210 40% 98%;
--ring: 215 20.2% 65.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71% 4%;
--foreground: 213 31% 91%;
--muted: 223 47% 11%;
--muted-foreground: 215.4 16.3% 56.9%;
--accent: 216 34% 17%;
--accent-foreground: 210 40% 98%;
--popover: 224 71% 4%;
--popover-foreground: 215 20.2% 65.1%;
--border: 216 34% 17%;
--input: 216 34% 17%;
--card: 224 71% 4%;
--card-foreground: 213 31% 91%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 1.2%;
--secondary: 222.2 47.4% 11.2%;
--secondary-foreground: 210 40% 98%;
--destructive: 0 63% 31%;
--destructive-foreground: 210 40% 98%;
--ring: 216 34% 17%;
--radius: 0.5rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}

View File

@ -1,9 +1,10 @@
import { env } from 'node:process';
import type { Provider } from '~/lib/stores/provider';
export function getAPIKey(cloudflareEnv: Env) {
/**
* The `cloudflareEnv` is only used when deployed or when previewing locally.
* In development the environment variables are available through `env`.
*/
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
export function getAPIKey(cloudflareEnv: Env, provider: Provider) {
if (provider === 'anthropic') {
return env.ANTHROPIC_API_KEY || cloudflareEnv.ANTHROPIC_API_KEY;
} else {
return env.TOGETHER_API_KEY || cloudflareEnv.TOGETHER_API_KEY;
}
}

View File

@ -0,0 +1,15 @@
import type { Provider } from '../../stores/provider';
export interface Env {
ANTHROPIC_API_KEY: string;
TOGETHER_API_KEY: string;
}
export function getAPIKey(env: Env, provider: Provider): string {
if (provider === 'anthropic') {
return env.ANTHROPIC_API_KEY;
} else if (typeof provider === 'object' && provider.type === 'together') {
return env.TOGETHER_API_KEY;
}
throw new Error(`Invalid provider: ${JSON.stringify(provider)}`);
}

View File

@ -1,4 +1,5 @@
import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from "@ai-sdk/openai";
export function getAnthropicModel(apiKey: string) {
const anthropic = createAnthropic({
@ -7,3 +8,18 @@ export function getAnthropicModel(apiKey: string) {
return anthropic('claude-3-5-sonnet-20240620');
}
export function getTogetherAIModel(apiKey: string, modelName: string) {
const together = createOpenAI({
apiKey,
baseURL: "https://api.together.xyz/v1",
});
return together(modelName);
}
export function getModel(provider: 'anthropic' | 'together', apiKey: string, modelName?: string) {
return provider === 'anthropic'
? getAnthropicModel(apiKey)
: getTogetherAIModel(apiKey, modelName || 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo');
}

View File

@ -95,6 +95,7 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
1. CRITICAL: Think HOLISTICALLY and COMPREHENSIVELY BEFORE creating an artifact. This means:
- Consider ALL relevant files in the project
- Review file paths when creating an entrypoint file or index.html to ensure it is pointing to the correct file (e.g., if creating a index.html and the entry file is in a subdirectory, the path should be relative to the entry file for example if files are in a src folder, the path should be relative to the src folder like this: <script type="module" src="/src/main.jsx"></script>).
- Review ALL previous file changes and user modifications (as shown in diffs, see diff_spec)
- Analyze the entire project context and dependencies
- Anticipate potential impacts on other parts of the system

View File

@ -1,8 +1,10 @@
import { streamText as _streamText, convertToCoreMessages } from 'ai';
import { getAPIKey } from '~/lib/.server/llm/api-key';
import { getAnthropicModel } from '~/lib/.server/llm/model';
import { getModel } from '~/lib/.server/llm/model';
import { MAX_TOKENS } from './constants';
import { getSystemPrompt } from './prompts';
import type { Env } from './env';
import type { Provider } from '~/lib/stores/provider';
interface ToolResult<Name extends string, Args, Result> {
toolCallId: string;
@ -21,15 +23,22 @@ export type Messages = Message[];
export type StreamingOptions = Omit<Parameters<typeof _streamText>[0], 'model'>;
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
export function streamText(messages: Messages, env: Env, provider: Provider, options?: StreamingOptions) {
const apiKey = getAPIKey(env, provider);
const model = provider === 'anthropic'
? getModel('anthropic', apiKey)
: getModel('together', apiKey, (provider as { type: 'together'; model: string }).model);
return _streamText({
model: getAnthropicModel(getAPIKey(env)),
model,
system: getSystemPrompt(),
maxTokens: MAX_TOKENS,
headers: {
'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15',
},
messages: convertToCoreMessages(messages),
messages: convertToCoreMessages(messages.map(message => ({
...message,
toolInvocations: message.toolInvocations?.map(invocation => ({
...invocation,
state: "result" as const
}))
}))),
...options,
});
}

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import { createScopedLogger } from '~/utils/logger';
import { providerStore } from '~/lib/stores/provider';
const logger = createScopedLogger('usePromptEnhancement');
@ -20,6 +21,7 @@ export function usePromptEnhancer() {
method: 'POST',
body: JSON.stringify({
message: input,
provider: providerStore.get(),
}),
});

View File

@ -0,0 +1,9 @@
import { atom } from 'nanostores';
export type Provider = 'anthropic' | { type: 'together', model: string };
export const providerStore = atom<Provider>('anthropic');
export function setProvider(provider: Provider) {
providerStore.set(provider);
}

6
app/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -10,6 +10,8 @@ import { useEffect } from 'react';
import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
import globalStyles from './styles/index.scss?url';
import xtermStyles from '@xterm/xterm/css/xterm.css?url';
// src/main.js or src/main.tsx
import './index.css';
import 'virtual:uno.css';

View File

@ -3,13 +3,14 @@ import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants';
import { CONTINUE_PROMPT } from '~/lib/.server/llm/prompts';
import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text';
import SwitchableStream from '~/lib/.server/llm/switchable-stream';
import type { Provider } from '~/lib/stores/provider';
export async function action(args: ActionFunctionArgs) {
return chatAction(args);
}
async function chatAction({ context, request }: ActionFunctionArgs) {
const { messages } = await request.json<{ messages: Messages }>();
const { messages, provider } = await request.json<{ messages: Messages; provider: Provider }>();
const stream = new SwitchableStream();
@ -22,7 +23,7 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
}
if (stream.switches >= MAX_RESPONSE_SEGMENTS) {
throw Error('Cannot continue message: Maximum segments reached');
throw new Error('Cannot continue message: Maximum segments reached');
}
const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches;
@ -32,25 +33,25 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
messages.push({ role: 'assistant', content });
messages.push({ role: 'user', content: CONTINUE_PROMPT });
const result = await streamText(messages, context.cloudflare.env, options);
const result = await streamText(messages, context.cloudflare.env, provider, options);
return stream.switchSource(result.toAIStream());
},
};
const result = await streamText(messages, context.cloudflare.env, options);
const result = await streamText(messages, context.cloudflare.env, provider, options);
stream.switchSource(result.toAIStream());
return new Response(stream.readable, {
status: 200,
headers: {
contentType: 'text/plain; charset=utf-8',
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error) {
console.log(error);
console.error('Chat action error:', error);
stream.close(); // Ensure the stream is closed on error
throw new Response(null, {
status: 500,
statusText: 'Internal Server Error',

View File

@ -1,6 +1,7 @@
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
import { StreamingTextResponse, parseStreamPart } from 'ai';
import { streamText } from '~/lib/.server/llm/stream-text';
import type { Provider } from '~/lib/stores/provider';
import { stripIndents } from '~/utils/stripIndent';
const encoder = new TextEncoder();
@ -11,7 +12,7 @@ export async function action(args: ActionFunctionArgs) {
}
async function enhancerAction({ context, request }: ActionFunctionArgs) {
const { message } = await request.json<{ message: string }>();
const { message, provider } = await request.json<{ message: string; provider: Provider }>();
try {
const result = await streamText(
@ -30,6 +31,7 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
},
],
context.cloudflare.env,
provider
);
const transformStream = new TransformStream({

24974
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.39",
"@ai-sdk/openai": "^0.0.66",
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/commands": "^6.6.0",
"@codemirror/lang-cpp": "^6.0.2",
@ -45,6 +46,8 @@
"@nanostores/react": "^0.7.2",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-slot": "^1.1.0",
"@remix-run/cloudflare": "^2.10.2",
"@remix-run/cloudflare-pages": "^2.10.2",
"@remix-run/react": "^2.10.2",
@ -55,12 +58,15 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"ai": "^3.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"diff": "^5.2.0",
"framer-motion": "^11.2.12",
"isbot": "^4.1.0",
"istextorbinary": "^9.5.0",
"jose": "^5.6.3",
"lucide-react": "^0.451.0",
"nanostores": "^0.10.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -74,6 +80,8 @@
"remix-island": "^0.2.0",
"remix-utils": "^7.6.0",
"shiki": "^1.9.1",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
@ -83,10 +91,14 @@
"@types/diff": "^5.2.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"autoprefixer": "^10.4.20",
"fast-glob": "^3.3.2",
"is-ci": "^3.0.1",
"node-fetch": "^3.3.2",
"postcss": "^8.4.47",
"prettier": "^3.3.2",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.5.2",
"unified": "^11.0.5",
"unocss": "^0.61.3",

76
tailwind.config.js Normal file
View File

@ -0,0 +1,76 @@
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,3 +1,4 @@
interface Env {
ANTHROPIC_API_KEY: string;
TOGETHER_API_KEY: string;
}