2024-12-01 14:29:23 -05:00

252 lines
7.5 KiB
TypeScript

import { TFolder, TFile } from "@/lib/types"
import React from "react"
// Stringify content for chat message component
export const stringifyContent = (
content: any,
seen = new WeakSet()
): string => {
// Stringify content if it's a string
if (typeof content === "string") {
return content
}
// Stringify content if it's null
if (content === null) {
return "null"
}
// Stringify content if it's undefined
if (content === undefined) {
return "undefined"
}
// Stringify content if it's a number or boolean
if (typeof content === "number" || typeof content === "boolean") {
return content.toString()
}
// Stringify content if it's a function
if (typeof content === "function") {
return content.toString()
}
// Stringify content if it's a symbol
if (typeof content === "symbol") {
return content.toString()
}
// Stringify content if it's a bigint
if (typeof content === "bigint") {
return content.toString() + "n"
}
// Stringify content if it's a valid React element
if (React.isValidElement(content)) {
return React.Children.toArray(
(content as React.ReactElement).props.children
)
.map((child) => stringifyContent(child, seen))
.join("")
}
// Stringify content if it's an array
if (Array.isArray(content)) {
return (
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
)
}
// Stringify content if it's an object
if (typeof content === "object") {
if (seen.has(content)) {
return "[Circular]"
}
seen.add(content)
try {
const pairs = Object.entries(content).map(
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
)
return "{" + pairs.join(", ") + "}"
} catch (error) {
return Object.prototype.toString.call(content)
}
}
// Stringify content if it's a primitive value
return String(content)
}
// Copy to clipboard for chat message component
export const copyToClipboard = (
text: string,
setCopiedText: (text: string | null) => void
) => {
// Copy text to clipboard for chat message component
navigator.clipboard.writeText(text).then(() => {
setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000)
})
}
// Handle send for chat message component
export const handleSend = async (
input: string,
context: string | null,
messages: any[],
setMessages: React.Dispatch<React.SetStateAction<any[]>>,
setInput: React.Dispatch<React.SetStateAction<string>>,
setIsContextExpanded: React.Dispatch<React.SetStateAction<boolean>>,
setIsGenerating: React.Dispatch<React.SetStateAction<boolean>>,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string,
isEditMode: boolean = false,
templateType: string,
files: (TFile | TFolder)[],
projectName: string
) => {
// Return if input is empty and context is null
if (input.trim() === "" && !context) return
// Get timestamp for chat message component
const timestamp = new Date()
.toLocaleTimeString("en-US", {
hour12: true,
hour: "2-digit",
minute: "2-digit",
})
.replace(/(\d{2}):(\d{2})/, "$1:$2")
// Create user message for chat message component
const userMessage = {
role: "user" as const,
content: input,
context: context || undefined,
timestamp: timestamp,
}
// Update messages for chat message component
const updatedMessages = [...messages, userMessage]
setMessages(updatedMessages)
setInput("")
setIsContextExpanded(false)
setIsGenerating(true)
setIsLoading(true)
abortControllerRef.current = new AbortController()
try {
// Create anthropic messages for chat message component
const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === "user" ? "human" : "assistant",
content: msg.content,
}))
// Fetch AI response for chat message component
const response = await fetch("/api/ai", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: anthropicMessages,
context: context || undefined,
activeFileContent: activeFileContent,
isEditMode: isEditMode,
templateType: templateType,
files: files,
projectName: projectName,
}),
signal: abortControllerRef.current.signal,
})
// Throw error if response is not ok
if (!response.ok) {
const error = await response.text()
throw new Error(error)
}
// Get reader for chat message component
const reader = response.body?.getReader()
const decoder = new TextDecoder()
const assistantMessage = { role: "assistant" as const, content: "" }
setMessages([...updatedMessages, assistantMessage])
setIsLoading(false)
// Initialize buffer for chat message component
let buffer = ""
const updateInterval = 100
let lastUpdateTime = Date.now()
// Read response from reader for chat message component
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const currentTime = Date.now()
if (currentTime - lastUpdateTime > updateInterval) {
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
lastUpdateTime = currentTime
}
}
// Update messages for chat message component
setMessages((prev) => {
const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer
return updatedMessages
})
}
} catch (error: any) {
// Handle abort error for chat message component
if (error.name === "AbortError") {
console.log("Generation aborted")
} else {
console.error("Error fetching AI response:", error)
const errorMessage = {
role: "assistant" as const,
content:
error.message || "Sorry, I encountered an error. Please try again.",
}
setMessages((prev) => [...prev, errorMessage])
}
} finally {
setIsGenerating(false)
setIsLoading(false)
abortControllerRef.current = null
}
}
// Handle stop generation for chat message component
export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null>
) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
}
// Check if text looks like code for chat message component
export const looksLikeCode = (text: string): boolean => {
const codeIndicators = [
/^import\s+/m, // import statements
/^function\s+/m, // function declarations
/^class\s+/m, // class declarations
/^const\s+/m, // const declarations
/^let\s+/m, // let declarations
/^var\s+/m, // var declarations
/[{}\[\]();]/, // common code syntax
/^\s*\/\//m, // comments
/^\s*\/\*/m, // multi-line comments
/=>/, // arrow functions
/^export\s+/m, // export statements
]
return codeIndicators.some((pattern) => pattern.test(text))
}
// Add this new function after looksLikeCode function
export const isFilePath = (text: string): boolean => {
// Match patterns like project/path/to/file.ext or project/path/to/file.ext (new file)
return /^[a-zA-Z0-9_-]+\/[\w/-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/.test(text)
}