Merge branch 'refs/heads/main' into feature/ai-chat

# Conflicts:
#	frontend/components/dashboard/newProject.tsx
#	frontend/components/editor/AIChat/ChatMessage.tsx
#	frontend/components/editor/AIChat/ContextDisplay.tsx
#	frontend/components/editor/AIChat/index.tsx
#	frontend/components/editor/index.tsx
#	frontend/components/editor/sidebar/index.tsx
#	frontend/components/editor/terminals/terminal.tsx
This commit is contained in:
James Murdza
2024-10-21 17:06:13 -06:00
29 changed files with 1778 additions and 1198 deletions

View File

@ -1,25 +1,31 @@
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';
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>
);
)
}

View File

@ -1,47 +1,60 @@
import { ChevronDown, ChevronUp, 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>
);
)
}

View File

@ -24,6 +24,7 @@ import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { Sandbox, TFile, TFolder, TTab, User } from "@/lib/types"
import {
addNew,
cn,
debounce,
deepMerge,
processFileType,
@ -926,7 +927,10 @@ export default function CodeEditor({
)}
</AnimatePresence>
</div>
<div className="z-50 p-1" ref={generateWidgetRef}>
<div
className={cn(generate.show && "z-50 p-1")}
ref={generateWidgetRef}
>
{generate.show ? (
<GenerateInput
user={userData}

View File

@ -84,8 +84,10 @@ export default function Loading({
</div>
</div>
<div className="w-full mt-1 flex flex-col">
<div className="w-full flex justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
<div className="w-full flex flex-col justify-center">
{new Array(6).fill(0).map((_, i) => (
<Skeleton key={i} className="h-[1.625rem] mb-0.5 rounded-sm" />
))}
</div>
</div>
</div>

View File

@ -1,20 +1,16 @@
"use client"
import { Button } from "@/components/ui/button"
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
import {
FilePlus,
FolderPlus,
Loader2,
MessageSquareMore,
Sparkles,
} from "lucide-react"
import { useEffect, useRef, useState } from "react"
import { FilePlus, FolderPlus, MessageSquareMore, Sparkles } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import SidebarFile from "./file"
import SidebarFolder from "./folder"
import New from "./new"
import Button from "@/components/ui/customButton"
import { Skeleton } from "@/components/ui/skeleton"
import { sortFileExplorer } from "@/lib/utils"
import {
dropTargetForElements,
monitorForElements,
@ -52,7 +48,9 @@ export default function Sidebar({
const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null)
const [movingId, setMovingId] = useState("")
const sortedFiles = useMemo(() => {
return sortFileExplorer(files)
}, [files])
useEffect(() => {
const el = ref.current
@ -133,13 +131,15 @@ export default function Sidebar({
isDraggedOver ? "bg-secondary/50" : ""
} rounded-sm w-full mt-1 flex flex-col`}
> */}
{files.length === 0 ? (
<div className="w-full flex justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
{sortedFiles.length === 0 ? (
<div className="w-full flex flex-col justify-center">
{new Array(6).fill(0).map((_, i) => (
<Skeleton key={i} className="h-[1.625rem] mb-0.5 rounded-sm" />
))}
</div>
) : (
<>
{files.map((child) =>
{sortedFiles.map((child) =>
child.type === "file" ? (
<SidebarFile
key={child.id}

View File

@ -4,8 +4,9 @@ import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import "./xterm.css"
import { debounce } from "@/lib/utils"
import { Loader2 } from "lucide-react"
import { useEffect, useRef } from "react"
import { ElementRef, useEffect, useRef } from "react"
import { Socket } from "socket.io-client"
export default function EditorTerminal({
@ -21,7 +22,8 @@ export default function EditorTerminal({
setTerm: (term: Terminal) => void
visible: boolean
}) {
const terminalRef = useRef(null)
const terminalRef = useRef<ElementRef<"div">>(null)
const fitAddonRef = useRef<FitAddon | null>(null)
useEffect(() => {
if (!terminalRef.current) return
@ -39,37 +41,61 @@ export default function EditorTerminal({
})
setTerm(terminal)
return () => {
if (terminal) terminal.dispose()
const dispose = () => {
terminal.dispose()
}
return dispose
}, [])
useEffect(() => {
if (!term) return
if (!terminalRef.current) return
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
term.open(terminalRef.current)
fitAddon.fit()
if (!fitAddonRef.current) {
const fitAddon = new FitAddon()
term.loadAddon(fitAddon)
term.open(terminalRef.current)
fitAddon.fit()
fitAddonRef.current = fitAddon
}
const disposableOnData = term.onData((data) => {
socket.emit("terminalData", id, data)
})
const disposableOnResize = term.onResize((dimensions) => {
// const terminal_size = {
// width: dimensions.cols,
// height: dimensions.rows,
// };
fitAddon.fit()
fitAddonRef.current?.fit()
socket.emit("terminalResize", dimensions)
})
const resizeObserver = new ResizeObserver(
debounce((entries) => {
if (!fitAddonRef.current || !terminalRef.current) return
const entry = entries[0]
if (!entry) return
const { width, height } = entry.contentRect
// Only call fit if the size has actually changed
if (
width !== terminalRef.current.offsetWidth ||
height !== terminalRef.current.offsetHeight
) {
try {
fitAddonRef.current.fit()
} catch (err) {
console.error("Error during fit:", err)
}
}
}, 50) // Debounce for 50ms
)
// start observing for resize
resizeObserver.observe(terminalRef.current)
return () => {
disposableOnData.dispose()
disposableOnResize.dispose()
resizeObserver.disconnect()
}
}, [term, terminalRef.current])