chore: format frontend code
This commit is contained in:
@ -1,36 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Send, StopCircle } from 'lucide-react';
|
||||
import { Send, StopCircle } from "lucide-react"
|
||||
import { Button } from "../../ui/button"
|
||||
|
||||
interface ChatInputProps {
|
||||
input: string;
|
||||
setInput: (input: string) => void;
|
||||
isGenerating: boolean;
|
||||
handleSend: () => void;
|
||||
handleStopGeneration: () => void;
|
||||
input: string
|
||||
setInput: (input: string) => void
|
||||
isGenerating: boolean
|
||||
handleSend: () => void
|
||||
handleStopGeneration: () => void
|
||||
}
|
||||
|
||||
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) {
|
||||
export default function ChatInput({
|
||||
input,
|
||||
setInput,
|
||||
isGenerating,
|
||||
handleSend,
|
||||
handleStopGeneration,
|
||||
}: ChatInputProps) {
|
||||
return (
|
||||
<div className="flex space-x-2 min-w-0">
|
||||
<input
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()}
|
||||
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
|
||||
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
||||
placeholder="Type your message..."
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
{isGenerating ? (
|
||||
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10">
|
||||
<Button
|
||||
onClick={handleStopGeneration}
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
>
|
||||
<StopCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10">
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={isGenerating}
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,25 +1,31 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { copyToClipboard, stringifyContent } from './lib/chatUtils';
|
||||
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
|
||||
import React, { useState } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { Button } from "../../ui/button"
|
||||
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||
|
||||
interface MessageProps {
|
||||
message: {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
context?: string;
|
||||
};
|
||||
setContext: (context: string | null) => void;
|
||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
setContext: (context: string | null) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
}
|
||||
|
||||
export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) {
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<number | null>(null);
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null);
|
||||
export default function ChatMessage({
|
||||
message,
|
||||
setContext,
|
||||
setIsContextExpanded,
|
||||
}: MessageProps) {
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||
|
||||
const renderCopyButton = (text: any) => (
|
||||
<Button
|
||||
@ -34,17 +40,17 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
)
|
||||
|
||||
const askAboutCode = (code: any) => {
|
||||
const contextString = stringifyContent(code);
|
||||
setContext(`Regarding this code:\n${contextString}`);
|
||||
setIsContextExpanded(false);
|
||||
};
|
||||
const contextString = stringifyContent(code)
|
||||
setContext(`Regarding this code:\n${contextString}`)
|
||||
setIsContextExpanded(false)
|
||||
}
|
||||
|
||||
const renderMarkdownElement = (props: any) => {
|
||||
const { node, children } = props;
|
||||
const content = stringifyContent(children);
|
||||
const { node, children } = props
|
||||
const content = stringifyContent(children)
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
@ -59,22 +65,30 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{React.createElement(node.tagName, {
|
||||
...props,
|
||||
className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors`
|
||||
}, children)}
|
||||
{React.createElement(
|
||||
node.tagName,
|
||||
{
|
||||
...props,
|
||||
className: `${
|
||||
props.className || ""
|
||||
} hover:bg-transparent rounded p-1 transition-colors`,
|
||||
},
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-left relative">
|
||||
<div className={`relative p-2 rounded-lg ${
|
||||
message.role === 'user'
|
||||
? 'bg-[#262626] text-white'
|
||||
: 'bg-transparent text-white'
|
||||
} max-w-full`}>
|
||||
{message.role === 'user' && (
|
||||
<div
|
||||
className={`relative p-2 rounded-lg ${
|
||||
message.role === "user"
|
||||
? "bg-[#262626] text-white"
|
||||
: "bg-transparent text-white"
|
||||
} max-w-full`}
|
||||
>
|
||||
{message.role === "user" && (
|
||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||
{renderCopyButton(message.content)}
|
||||
<Button
|
||||
@ -89,13 +103,13 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
)}
|
||||
{message.context && (
|
||||
<div className="mb-2 bg-input rounded-lg">
|
||||
<div
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
|
||||
onClick={() =>
|
||||
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
|
||||
}
|
||||
>
|
||||
<span className="text-sm text-gray-300">
|
||||
Context
|
||||
</span>
|
||||
<span className="text-sm text-gray-300">Context</span>
|
||||
{expandedMessageIndex === 0 ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
@ -105,41 +119,46 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
{expandedMessageIndex === 0 && (
|
||||
<div className="relative">
|
||||
<div className="absolute top-0 right-0 flex p-1">
|
||||
{renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))}
|
||||
{renderCopyButton(
|
||||
message.context.replace(/^Regarding this code:\n/, "")
|
||||
)}
|
||||
</div>
|
||||
{(() => {
|
||||
const code = message.context.replace(/^Regarding this code:\n/, '');
|
||||
const match = /language-(\w+)/.exec(code);
|
||||
const language = match ? match[1] : 'typescript';
|
||||
const code = message.context.replace(
|
||||
/^Regarding this code:\n/,
|
||||
""
|
||||
)
|
||||
const match = /language-(\w+)/.exec(code)
|
||||
const language = match ? match[1] : "typescript"
|
||||
return (
|
||||
<div className="pt-6">
|
||||
<textarea
|
||||
value={code}
|
||||
onChange={(e) => {
|
||||
const updatedContext = `Regarding this code:\n${e.target.value}`;
|
||||
setContext(updatedContext);
|
||||
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||
setContext(updatedContext)
|
||||
}}
|
||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||
rows={code.split('\n').length}
|
||||
rows={code.split("\n").length}
|
||||
style={{
|
||||
resize: 'vertical',
|
||||
minHeight: '100px',
|
||||
maxHeight: '400px',
|
||||
resize: "vertical",
|
||||
minHeight: "100px",
|
||||
maxHeight: "400px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{message.role === 'assistant' ? (
|
||||
{message.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({node, className, children, ...props}) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
code({ node, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
@ -163,8 +182,8 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
@ -175,7 +194,7 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
)
|
||||
},
|
||||
p: renderMarkdownElement,
|
||||
h1: renderMarkdownElement,
|
||||
@ -184,18 +203,24 @@ export default function ChatMessage({ message, setContext, setIsContextExpanded
|
||||
h4: renderMarkdownElement,
|
||||
h5: renderMarkdownElement,
|
||||
h6: renderMarkdownElement,
|
||||
ul: (props) => <ul className="list-disc pl-6 mb-4 space-y-2">{props.children}</ul>,
|
||||
ol: (props) => <ol className="list-decimal pl-6 mb-4 space-y-2">{props.children}</ol>,
|
||||
ul: (props) => (
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap group">
|
||||
{message.content}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap group">{message.content}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,48 +1,60 @@
|
||||
import React from 'react';
|
||||
import { ChevronUp, ChevronDown, X } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||
|
||||
interface ContextDisplayProps {
|
||||
context: string | null;
|
||||
isContextExpanded: boolean;
|
||||
setIsContextExpanded: (isExpanded: boolean) => void;
|
||||
setContext: (context: string | null) => void;
|
||||
context: string | null
|
||||
isContextExpanded: boolean
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
setContext: (context: string | null) => void
|
||||
}
|
||||
|
||||
export default function ContextDisplay({ context, isContextExpanded, setIsContextExpanded, setContext }: ContextDisplayProps) {
|
||||
if (!context) return null;
|
||||
export default function ContextDisplay({
|
||||
context,
|
||||
isContextExpanded,
|
||||
setIsContextExpanded,
|
||||
setContext,
|
||||
}: ContextDisplayProps) {
|
||||
if (!context) return null
|
||||
|
||||
return (
|
||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<div
|
||||
className="flex-grow cursor-pointer"
|
||||
<div
|
||||
className="flex-grow cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||||
>
|
||||
<span className="text-sm text-gray-300">
|
||||
Context
|
||||
</span>
|
||||
<span className="text-sm text-gray-300">Context</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isContextExpanded ? (
|
||||
<ChevronUp size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(false)} />
|
||||
<ChevronUp
|
||||
size={16}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDown size={16} className="cursor-pointer" onClick={() => setIsContextExpanded(true)} />
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
<X
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
||||
<X
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
||||
onClick={() => setContext(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isContextExpanded && (
|
||||
<textarea
|
||||
value={context.replace(/^Regarding this code:\n/, '')}
|
||||
onChange={(e) => setContext(`Regarding this code:\n${e.target.value}`)}
|
||||
value={context.replace(/^Regarding this code:\n/, "")}
|
||||
onChange={(e) =>
|
||||
setContext(`Regarding this code:\n${e.target.value}`)
|
||||
}
|
||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
||||
rows={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,52 +1,66 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import LoadingDots from '../../ui/LoadingDots';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import ChatInput from './ChatInput';
|
||||
import ContextDisplay from './ContextDisplay';
|
||||
import { handleSend, handleStopGeneration } from './lib/chatUtils';
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import LoadingDots from "../../ui/LoadingDots"
|
||||
import ChatInput from "./ChatInput"
|
||||
import ChatMessage from "./ChatMessage"
|
||||
import ContextDisplay from "./ContextDisplay"
|
||||
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
context?: string;
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
export default function AIChat({ activeFileContent, activeFileName }: { activeFileContent: string, activeFileName: string }) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [context, setContext] = useState<string | null>(null);
|
||||
const [isContextExpanded, setIsContextExpanded] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
export default function AIChat({
|
||||
activeFileContent,
|
||||
activeFileName,
|
||||
}: {
|
||||
activeFileContent: string
|
||||
activeFileName: string
|
||||
}) {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [input, setInput] = useState("")
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
const [context, setContext] = useState<string | null>(null)
|
||||
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (chatContainerRef.current) {
|
||||
setTimeout(() => {
|
||||
chatContainerRef.current?.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
behavior: "smooth",
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
<span className="text-muted-foreground/50 font-medium">CHAT</span>
|
||||
<span className="text-muted-foreground/50 font-medium truncate max-w-[50%]" title={activeFileName}>{activeFileName}</span>
|
||||
<span
|
||||
className="text-muted-foreground/50 font-medium truncate max-w-[50%]"
|
||||
title={activeFileName}
|
||||
>
|
||||
{activeFileName}
|
||||
</span>
|
||||
</div>
|
||||
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||
>
|
||||
{messages.map((message, messageIndex) => (
|
||||
<ChatMessage
|
||||
key={messageIndex}
|
||||
message={message}
|
||||
<ChatMessage
|
||||
key={messageIndex}
|
||||
message={message}
|
||||
setContext={setContext}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
/>
|
||||
@ -54,20 +68,33 @@ export default function AIChat({ activeFileContent, activeFileName }: { activeFi
|
||||
{isLoading && <LoadingDots />}
|
||||
</div>
|
||||
<div className="p-4 border-t mb-14">
|
||||
<ContextDisplay
|
||||
context={context}
|
||||
<ContextDisplay
|
||||
context={context}
|
||||
isContextExpanded={isContextExpanded}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
setContext={setContext}
|
||||
/>
|
||||
<ChatInput
|
||||
<ChatInput
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
isGenerating={isGenerating}
|
||||
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)}
|
||||
handleSend={() =>
|
||||
handleSend(
|
||||
input,
|
||||
context,
|
||||
messages,
|
||||
setMessages,
|
||||
setInput,
|
||||
setIsContextExpanded,
|
||||
setIsGenerating,
|
||||
setIsLoading,
|
||||
abortControllerRef,
|
||||
activeFileContent
|
||||
)
|
||||
}
|
||||
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@ -1,58 +1,68 @@
|
||||
import React from 'react';
|
||||
import React from "react"
|
||||
|
||||
export const stringifyContent = (content: any, seen = new WeakSet()): string => {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
export const stringifyContent = (
|
||||
content: any,
|
||||
seen = new WeakSet()
|
||||
): string => {
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
if (content === null) {
|
||||
return 'null';
|
||||
return "null"
|
||||
}
|
||||
if (content === undefined) {
|
||||
return 'undefined';
|
||||
return "undefined"
|
||||
}
|
||||
if (typeof content === 'number' || typeof content === 'boolean') {
|
||||
return content.toString();
|
||||
if (typeof content === "number" || typeof content === "boolean") {
|
||||
return content.toString()
|
||||
}
|
||||
if (typeof content === 'function') {
|
||||
return content.toString();
|
||||
if (typeof content === "function") {
|
||||
return content.toString()
|
||||
}
|
||||
if (typeof content === 'symbol') {
|
||||
return content.toString();
|
||||
if (typeof content === "symbol") {
|
||||
return content.toString()
|
||||
}
|
||||
if (typeof content === 'bigint') {
|
||||
return content.toString() + 'n';
|
||||
if (typeof content === "bigint") {
|
||||
return content.toString() + "n"
|
||||
}
|
||||
if (React.isValidElement(content)) {
|
||||
return React.Children.toArray((content as React.ReactElement).props.children)
|
||||
.map(child => stringifyContent(child, seen))
|
||||
.join('');
|
||||
return React.Children.toArray(
|
||||
(content as React.ReactElement).props.children
|
||||
)
|
||||
.map((child) => stringifyContent(child, seen))
|
||||
.join("")
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']';
|
||||
return (
|
||||
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
|
||||
)
|
||||
}
|
||||
if (typeof content === 'object') {
|
||||
if (typeof content === "object") {
|
||||
if (seen.has(content)) {
|
||||
return '[Circular]';
|
||||
return "[Circular]"
|
||||
}
|
||||
seen.add(content);
|
||||
seen.add(content)
|
||||
try {
|
||||
const pairs = Object.entries(content).map(
|
||||
([key, value]) => `${key}: ${stringifyContent(value, seen)}`
|
||||
);
|
||||
return '{' + pairs.join(', ') + '}';
|
||||
)
|
||||
return "{" + pairs.join(", ") + "}"
|
||||
} catch (error) {
|
||||
return Object.prototype.toString.call(content);
|
||||
return Object.prototype.toString.call(content)
|
||||
}
|
||||
}
|
||||
return String(content);
|
||||
};
|
||||
return String(content)
|
||||
}
|
||||
|
||||
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => {
|
||||
export const copyToClipboard = (
|
||||
text: string,
|
||||
setCopiedText: (text: string | null) => void
|
||||
) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedText(text);
|
||||
setTimeout(() => setCopiedText(null), 2000);
|
||||
});
|
||||
};
|
||||
setCopiedText(text)
|
||||
setTimeout(() => setCopiedText(null), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
export const handleSend = async (
|
||||
input: string,
|
||||
@ -66,97 +76,105 @@ export const handleSend = async (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string
|
||||
) => {
|
||||
if (input.trim() === '' && !context) return;
|
||||
if (input.trim() === "" && !context) return
|
||||
|
||||
const newMessage = {
|
||||
role: 'user' as const,
|
||||
const newMessage = {
|
||||
role: "user" as const,
|
||||
content: input,
|
||||
context: context || undefined
|
||||
};
|
||||
const updatedMessages = [...messages, newMessage];
|
||||
setMessages(updatedMessages);
|
||||
setInput('');
|
||||
setIsContextExpanded(false);
|
||||
setIsGenerating(true);
|
||||
setIsLoading(true);
|
||||
context: context || undefined,
|
||||
}
|
||||
const updatedMessages = [...messages, newMessage]
|
||||
setMessages(updatedMessages)
|
||||
setInput("")
|
||||
setIsContextExpanded(false)
|
||||
setIsGenerating(true)
|
||||
setIsLoading(true)
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
const anthropicMessages = updatedMessages.map(msg => ({
|
||||
role: msg.role === 'user' ? 'human' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||
role: msg.role === "user" ? "human" : "assistant",
|
||||
content: msg.content,
|
||||
}))
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get AI response');
|
||||
throw new Error("Failed to get AI response")
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const assistantMessage = { role: 'assistant' as const, content: '' };
|
||||
setMessages([...updatedMessages, assistantMessage]);
|
||||
setIsLoading(false);
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||
setMessages([...updatedMessages, assistantMessage])
|
||||
setIsLoading(false)
|
||||
|
||||
let buffer = '';
|
||||
const updateInterval = 100;
|
||||
let lastUpdateTime = Date.now();
|
||||
let buffer = ""
|
||||
const updateInterval = 100
|
||||
let lastUpdateTime = Date.now()
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const currentTime = Date.now();
|
||||
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;
|
||||
setMessages((prev) => {
|
||||
const updatedMessages = [...prev]
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||
lastMessage.content = buffer
|
||||
return updatedMessages
|
||||
})
|
||||
lastUpdateTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
||||
lastMessage.content = buffer;
|
||||
return updatedMessages;
|
||||
});
|
||||
setMessages((prev) => {
|
||||
const updatedMessages = [...prev]
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||
lastMessage.content = buffer
|
||||
return updatedMessages
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('Generation aborted');
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Generation aborted")
|
||||
} else {
|
||||
console.error('Error fetching AI response:', error);
|
||||
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' };
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
console.error("Error fetching AI response:", error)
|
||||
const errorMessage = {
|
||||
role: "assistant" as const,
|
||||
content: "Sorry, I encountered an error. Please try again.",
|
||||
}
|
||||
setMessages((prev) => [...prev, errorMessage])
|
||||
}
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setIsLoading(false);
|
||||
abortControllerRef.current = null;
|
||||
setIsGenerating(false)
|
||||
setIsLoading(false)
|
||||
abortControllerRef.current = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => {
|
||||
export const handleStopGeneration = (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||
) => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user