Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

39 changes: 25 additions & 14 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { toPrompt } from '@/lib/prompt'
import ratelimit from '@/lib/ratelimit'
import { fragmentSchema as schema } from '@/lib/schema'
import { Templates } from '@/lib/templates'
import { streamObject, LanguageModel, CoreMessage } from 'ai'
import { streamText, Output, LanguageModel, type ModelMessage } from 'ai'

export const maxDuration = 300

Expand All @@ -25,7 +25,7 @@ export async function POST(req: Request) {
model,
config,
}: {
messages: CoreMessage[]
messages: ModelMessage[]
userID: string | undefined
teamID: string | undefined
template: Templates
Expand Down Expand Up @@ -54,18 +54,29 @@ export async function POST(req: Request) {
const { model: modelNameString, apiKey: modelApiKey, ...modelParams } = config
const modelClient = getModelClient(model, config)

try {
const stream = await streamObject({
model: modelClient as LanguageModel,
schema,
system: toPrompt(template),
messages,
maxRetries: 0, // do not retry on errors
...modelParams,
})
let apiError: any = null

const result = streamText({
model: modelClient as LanguageModel,
output: Output.object({ schema }),
system: toPrompt(template),
messages,
maxRetries: 0,
onError: ({ error }) => {
apiError = error
},
...modelParams,
})

return stream.toTextStreamResponse()
} catch (error: any) {
return handleAPIError(error, { hasOwnApiKey: !!config.apiKey })
// Check if API call succeeds by awaiting first chunk
try {
await result.response
} catch {
// apiError is set by onError callback with the actual API error
if (apiError) {
return handleAPIError(apiError, { hasOwnApiKey: !!config.apiKey })
}
}

return result.toTextStreamResponse()
}
14 changes: 7 additions & 7 deletions app/api/morph-chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Duration } from '@/lib/duration'
import { getModelClient, LLMModel, LLMModelConfig } from '@/lib/models'
import { applyPatch } from '@/lib/morph'
import ratelimit from '@/lib/ratelimit'
import { FragmentSchema, morphEditSchema, MorphEditSchema } from '@/lib/schema'
import { generateObject, LanguageModel, CoreMessage } from 'ai'
import { FragmentSchema, morphEditSchema } from '@/lib/schema'
import { generateText, Output, LanguageModel, type ModelMessage } from 'ai'

export const maxDuration = 300

Expand All @@ -24,7 +24,7 @@ export async function POST(req: Request) {
config,
currentFragment,
}: {
messages: CoreMessage[]
messages: ModelMessage[]
model: LLMModel
config: LLMModelConfig
currentFragment: FragmentSchema
Expand Down Expand Up @@ -64,16 +64,16 @@ ${currentFragment.code}

`

const result = await generateObject({
const result = await generateText({
model: modelClient as LanguageModel,
system: contextualSystemPrompt,
messages,
schema: morphEditSchema,
output: Output.object({ schema: morphEditSchema }),
maxRetries: 0,
...modelParams,
})

const editInstructions = result.object
const editInstructions = result.output!

// Apply edits using Morph
const morphResult = await applyPatch({
Expand Down Expand Up @@ -105,7 +105,7 @@ ${currentFragment.code}
'Content-Type': 'text/plain; charset=utf-8',
},
})
} catch (error: any) {
} catch (error: unknown) {
return handleAPIError(error, { hasOwnApiKey: !!config.apiKey })
}
}
55 changes: 29 additions & 26 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { supabase } from '@/lib/supabase'
import templates from '@/lib/templates'
import { ExecutionResult } from '@/lib/types'
import { DeepPartial } from 'ai'
import { experimental_useObject as useObject } from 'ai/react'
import { experimental_useObject as useObject } from '@ai-sdk/react'
import { usePostHog } from 'posthog-js/react'
import { SetStateAction, useEffect, useState } from 'react'
import { useLocalStorage } from 'usehooks-ts'
Expand Down Expand Up @@ -72,6 +72,7 @@ export default function Home() {
if (languageModel.model && !filteredModels.find((m) => m.id === languageModel.model)) {
setLanguageModel({ ...languageModel, model: defaultModel.id })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [languageModel.model])
const currentTemplate =
selectedTemplate === 'auto'
Expand All @@ -84,6 +85,31 @@ export default function Home() {
useMorphApply && fragment && fragment.code && fragment.file_path
const apiEndpoint = shouldUseMorph ? '/api/morph-chat' : '/api/chat'

function setMessage(message: Partial<Message>, index?: number) {
setMessages((previousMessages) => {
const updatedMessages = [...previousMessages]
updatedMessages[index ?? previousMessages.length - 1] = {
...previousMessages[index ?? previousMessages.length - 1],
...message,
}

return updatedMessages
})
}

function addMessage(message: Message) {
setMessages((previousMessages) => [...previousMessages, message])
return [...messages, message]
}

function setCurrentPreview(preview: {
fragment: DeepPartial<FragmentSchema> | undefined
result: ExecutionResult | undefined
}) {
setFragment(preview.fragment)
setResult(preview.result)
}

const { object, submit, isLoading, stop, error } = useObject({
api: apiEndpoint,
schema,
Expand Down Expand Up @@ -150,24 +176,14 @@ export default function Home() {
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [object])

useEffect(() => {
if (error) stop()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])

function setMessage(message: Partial<Message>, index?: number) {
setMessages((previousMessages) => {
const updatedMessages = [...previousMessages]
updatedMessages[index ?? previousMessages.length - 1] = {
...previousMessages[index ?? previousMessages.length - 1],
...message,
}

return updatedMessages
})
}

async function handleSubmitAuth(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()

Expand Down Expand Up @@ -225,11 +241,6 @@ export default function Home() {
})
}

function addMessage(message: Message) {
setMessages((previousMessages) => [...previousMessages, message])
return [...messages, message]
}

function handleSaveInputChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
setChatInput(e.target.value)
}
Expand Down Expand Up @@ -271,14 +282,6 @@ export default function Home() {
setIsPreviewLoading(false)
}

function setCurrentPreview(preview: {
fragment: DeepPartial<FragmentSchema> | undefined
result: ExecutionResult | undefined
}) {
setFragment(preview.fragment)
setResult(preview.result)
}

function handleUndo() {
setMessages((previousMessages) => [...previousMessages.slice(0, -2)])
setCurrentPreview({ fragment: undefined, result: undefined })
Expand Down
3 changes: 1 addition & 2 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client'

import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes'
import posthog from 'posthog-js'
import { PostHogProvider as PostHogProviderJS } from 'posthog-js/react'

Expand Down
4 changes: 3 additions & 1 deletion components/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ function Auth({
magicLink = false,
onSignUpValidate,
metadata,
}: AuthProps): JSX.Element | null {
}: AuthProps): React.ReactNode {
const [authView, setAuthView] = useState<ViewType>(view)
const {
loading,
Expand All @@ -571,11 +571,13 @@ function Auth({
clearMessages,
} = useAuthForm()

/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
setAuthView(view)
setError(null)
setMessage(null)
}, [view, setError, setMessage])
/* eslint-enable react-hooks/set-state-in-effect */

const setAuthViewAndClearMessages = useCallback(
(newView: ViewType) => {
Expand Down
3 changes: 3 additions & 0 deletions components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export function ChatInput({
>
<X className="h-3 w-3 cursor-pointer" />
</span>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={URL.createObjectURL(file)}
alt={file.name}
Expand All @@ -124,6 +125,7 @@ export function ChatInput({
</div>
)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [files])

function onEnter(e: React.KeyboardEvent<HTMLFormElement>) {
Expand All @@ -141,6 +143,7 @@ export function ChatInput({
if (!isMultiModal) {
handleFileChange([])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMultiModal])

return (
Expand Down
4 changes: 2 additions & 2 deletions components/chat-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,15 @@ export function ChatSettings({
</span>
<Input
type="number"
defaultValue={languageModel.maxTokens}
defaultValue={languageModel.maxOutputTokens}
min={50}
max={10000}
step={1}
className="h-6 rounded-sm w-[84px] text-xs text-center tabular-nums"
placeholder="Auto"
onChange={(e) =>
onLanguageModelChange({
maxTokens: parseFloat(e.target.value) || undefined,
maxOutputTokens: parseFloat(e.target.value) || undefined,
})
}
/>
Expand Down
2 changes: 2 additions & 0 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function Chat({
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(messages)])

return (
Expand All @@ -40,6 +41,7 @@ export function Chat({
}
if (content.type === 'image') {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
key={id}
src={content.image}
Expand Down
1 change: 1 addition & 0 deletions components/deploy-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function DeployDialog({
const [duration, setDuration] = useState<string | null>(null)

useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setPublishedURL(null)
}, [url])

Expand Down
1 change: 1 addition & 0 deletions components/ui/theme-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ThemeToggle = forwardRef<

// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true)
}, [])

Expand Down
16 changes: 16 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import nextConfig from 'eslint-config-next'

const eslintConfig = [
...nextConfig,
{
ignores: [
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
],
},
]

export default eslintConfig
24 changes: 17 additions & 7 deletions lib/api-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,41 @@ export interface APIError {
message: string
}

/**
* Gets the status code from an error
*/
function getStatusCode(error: any): number | undefined {
return error?.statusCode ?? error?.status
}

/**
* Checks if an error is a rate limit error
*/
export function isRateLimitError(error: any): boolean {
const status = getStatusCode(error)
return (
error &&
(error.statusCode === 429 ||
error.message.toLowerCase().includes('limit') ||
error.message.toLowerCase().includes('billing'))
(status === 429 ||
error.message?.toLowerCase().includes('limit') ||
error.message?.toLowerCase().includes('billing'))
)
}

/**
* Checks if an error is an overloaded/unavailable error
*/
export function isOverloadedError(error: any): boolean {
return error && (error.statusCode === 529 || error.statusCode === 503)
const status = getStatusCode(error)
return error && (status === 529 || status === 503)
}

/**
* Checks if an error is an access denied/unauthorized error
*/
export function isAccessDeniedError(error: any): boolean {
return error && (error.statusCode === 403 || error.statusCode === 401)
const status = getStatusCode(error)
const message = error?.message?.toLowerCase() || ''
return error && (status === 403 || status === 401 || message.includes('api key') || message.includes('x-api-key') || message.includes('unauthorized'))
}

/**
Expand All @@ -40,8 +51,7 @@ export function handleAPIError(
error: any,
context?: { hasOwnApiKey?: boolean },
): Response {
// Log the error for debugging
console.error('API Error:', error)
console.error('API Error:', error?.message || error)

if (isRateLimitError(error)) {
const message = context?.hasOwnApiKey
Expand Down
Loading