mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
Add Together AI model support
This commit is contained in:
parent
cecbc55380
commit
a93e91c433
@ -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`.
|
||||
|
||||
@ -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]" />
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
60
app/components/chat/ProviderSelector.tsx
Normal file
60
app/components/chat/ProviderSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
57
app/components/ui/button.tsx
Normal file
57
app/components/ui/button.tsx
Normal 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 }
|
||||
205
app/components/ui/dropdown-menu.tsx
Normal file
205
app/components/ui/dropdown-menu.tsx
Normal 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
81
app/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
15
app/lib/.server/llm/env.ts
Normal file
15
app/lib/.server/llm/env.ts
Normal 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)}`);
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
9
app/lib/stores/provider.ts
Normal file
9
app/lib/stores/provider.ts
Normal 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
6
app/lib/utils.ts
Normal 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))
|
||||
}
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
24974
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -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
76
tailwind.config.js
Normal 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")],
|
||||
}
|
||||
1
worker-configuration.d.ts
vendored
1
worker-configuration.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
interface Env {
|
||||
ANTHROPIC_API_KEY: string;
|
||||
TOGETHER_API_KEY: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user