Compare commits

..

11 Commits

Author SHA1 Message Date
f6fee20955 chore: update .env.example 2024-12-06 14:24:06 -05:00
74b0c6b9e4 fix: apply code- discard button 2024-12-01 22:15:03 -05:00
d840aad3e9 feat: Add clickable file paths in AI chat responses
- Detect file paths with dots in directory names (e.g. next/styles/SignIn.module.css)
- Create new files when path ends with "(new file)"
- Use existing socket connection and file management system
2024-12-01 18:52:02 -05:00
89804c5906 feat: file path above each code snippet 2024-12-01 14:28:48 -05:00
aa33ad3031 feat: introduce apply button functionality (v0.1)
### Summary
- Added a new "Apply" button to code snippets provided by the AI assistant.
- The button is designed to seamlessly merge the AI-generated snippet into the relevant file in the editor.

### Current Issues
1. **Sticky Accept/Decline Buttons:** These activate for every snippet instead of being limited to the relevant snippet.
2. **Discard Button:** Currently non-functional.
3. **Highlight Inconsistencies:** The green-red code highlights for old and new code are inconsistent.

### To Do
- Implement a toast notification when the "Apply" button is pressed on an irrelevant tab to prevent code application errors.

### Workflow Implemented
1. The "Apply" button is added alongside "Copy" and "Reply" for AI-generated code snippets.
2. Upon clicking "Apply," the code snippet and relevant file content (active file) are sent to a secondary model (GPT-4O).
3. The system prompt for GPT-4O instructs it to merge the snippet with the file content:
   - Ensure the original file functionality remains intact.
   - Integrate the code snippet seamlessly.
4. The output from GPT-4O is injected directly into the code editor.
5. Changes are visually highlighted:
   - Green for new code.
   - Red for removed code.
6. Highlights remain until the user explicitly accepts or discards the changes.
2024-11-30 20:56:56 -05:00
f79115974c feat: add dynamic file structure context in AI chat
- Improved file structure formatting with tree-like visualization
- Added filtering for ignored files and folders
- Added scripts section to template context
- Fixed folder hierarchy display with proper indentation
- Maintains sorting with folders first, then files alphabetically
- Now uses actual project files instead of template structure

Example output:
├── app/
│   ├── api/
│   └── page.tsx
├── components/
└── package.json
2024-11-30 15:53:17 -05:00
1bb518b1af chore: context tab updates
- Context tab updates with the latest file changes and will not be removed from context when a message is sent
2024-11-30 02:26:34 -05:00
721ea5cad2 feat: sticky copy-reply button on chat code-snippets 2024-11-30 01:33:54 -05:00
ebfde291ad fix: terminal paste functionality 2024-11-30 00:03:51 -05:00
07ca7dee46 fix: image handling in context 2024-11-29 21:49:44 -05:00
5af35f6920 chore: formatting the code of recent changes 2024-11-29 13:06:12 -05:00
17 changed files with 188 additions and 175 deletions

View File

@ -22,7 +22,6 @@ Needed accounts to set up:
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing. - [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
- [E2B](https://e2b.dev/): Used for the terminals and live preview. - [E2B](https://e2b.dev/): Used for the terminals and live preview.
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2). - [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
- [Anthropic](https://anthropic.com/) and [OpenAI](https://openai.com/): API keys for code generation.
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io. A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
@ -153,7 +152,6 @@ NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev' NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET' NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
ANTHROPIC_API_KEY='🔑' ANTHROPIC_API_KEY='🔑'
OPENAI_API_KEY='🔑'
``` ```
### 10. Running the IDE ### 10. Running the IDE
@ -258,6 +256,7 @@ backend/
| `backend/server` | The Express websocket server. | | `backend/server` | The Express websocket server. |
| `backend/database` | API for interfacing with the D1 database (SQLite). | | `backend/database` | API for interfacing with the D1 database (SQLite). |
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. | | `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
| `backend/ai` | API for making requests to Workers AI . |
### Development ### Development

View File

@ -7,7 +7,6 @@ PORT=4000
WORKERS_KEY= WORKERS_KEY=
DATABASE_WORKER_URL= DATABASE_WORKER_URL=
STORAGE_WORKER_URL= STORAGE_WORKER_URL=
AI_WORKER_URL=
E2B_API_KEY= E2B_API_KEY=
DOKKU_HOST= DOKKU_HOST=
DOKKU_USERNAME= DOKKU_USERNAME=

View File

@ -31,30 +31,9 @@ export class DokkuClient extends SSHSocketClient {
// List all deployed Dokku apps // List all deployed Dokku apps
async listApps(): Promise<string[]> { async listApps(): Promise<string[]> {
const response = await this.sendCommand("--quiet apps:list") const response = await this.sendCommand("apps:list")
return response.output.split("\n") // Split the output by newline and remove the header
} return response.output.split("\n").slice(1)
// Get the creation timestamp of an app
async getAppCreatedAt(appName: string): Promise<number> {
const response = await this.sendCommand(
`apps:report --app-created-at ${appName}`
)
const createdAt = parseInt(response.output.trim(), 10)
if (isNaN(createdAt)) {
throw new Error(
`Failed to retrieve creation timestamp for app ${appName}`
)
}
return createdAt
}
// Check if an app exists
async appExists(appName: string): Promise<boolean> {
const response = await this.sendCommand(`apps:exists ${appName}`)
return response.output.includes("App") === false
} }
} }

View File

@ -156,28 +156,6 @@ export class Sandbox {
return { success: true, apps: await this.dokkuClient.listApps() } return { success: true, apps: await this.dokkuClient.listApps() }
} }
// Handle getting app creation timestamp
const handleGetAppCreatedAt: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient)
throw new Error(
"Failed to retrieve app creation timestamp: No Dokku client"
)
return {
success: true,
createdAt: await this.dokkuClient.getAppCreatedAt(appName),
}
}
// Handle checking if an app exists
const handleAppExists: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient)
throw new Error("Failed to check app existence: No Dokku client")
return {
success: true,
exists: await this.dokkuClient.appExists(appName),
}
}
// Handle deploying code // Handle deploying code
const handleDeploy: SocketHandler = async (_: any) => { const handleDeploy: SocketHandler = async (_: any) => {
if (!this.gitClient) throw Error("No git client") if (!this.gitClient) throw Error("No git client")
@ -275,9 +253,7 @@ export class Sandbox {
getFolder: handleGetFolder, getFolder: handleGetFolder,
saveFile: handleSaveFile, saveFile: handleSaveFile,
moveFile: handleMoveFile, moveFile: handleMoveFile,
listApps: handleListApps, list: handleListApps,
getAppCreatedAt: handleGetAppCreatedAt,
getAppExists: handleAppExists,
deploy: handleDeploy, deploy: handleDeploy,
createFile: handleCreateFile, createFile: handleCreateFile,
createFolder: handleCreateFolder, createFolder: handleCreateFolder,

View File

@ -146,8 +146,6 @@ io.on("connection", async (socket) => {
) )
}) })
socket.emit("ready")
// Handle disconnection event // Handle disconnection event
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
try { try {

View File

@ -3,18 +3,21 @@ CLERK_SECRET_KEY=
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
LIVEBLOCKS_SECRET_KEY= LIVEBLOCKS_SECRET_KEY=
NEXT_PUBLIC_SERVER_URL=http://localhost:4000 NEXT_PUBLIC_SERVER_PORT=4000
NEXT_PUBLIC_APP_URL=http://localhost:3000 NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
# Set WORKER_URLs after deploying the workers. # Set WORKER_URLs after deploying the workers.
# Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml. # Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
# These URLs should begin with https:// in production NEXT_PUBLIC_DATABASE_WORKER_URL=https://database.your-worker.workers.dev
NEXT_PUBLIC_DATABASE_WORKER_URL= NEXT_PUBLIC_STORAGE_WORKER_URL=https://storage.your-worker.workers.dev
NEXT_PUBLIC_STORAGE_WORKER_URL= NEXT_PUBLIC_WORKERS_KEY=SUPERDUPERSECRET
NEXT_PUBLIC_AI_WORKER_URL=
NEXT_PUBLIC_WORKERS_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
ANTHROPIC_API_KEY=
OPENAI_API_KEY=

View File

@ -1,16 +1,22 @@
import { currentUser } from "@clerk/nextjs" import {
import { Anthropic } from "@anthropic-ai/sdk" ignoredFiles,
import { TIERS } from "@/lib/tiers" ignoredFolders,
} from "@/components/editor/AIChat/lib/ignored-paths"
import { templateConfigs } from "@/lib/templates" import { templateConfigs } from "@/lib/templates"
import { TIERS } from "@/lib/tiers"
import { TFile, TFolder } from "@/lib/types" import { TFile, TFolder } from "@/lib/types"
import { ignoredFiles, ignoredFolders } from "@/components/editor/AIChat/lib/ignored-paths" import { Anthropic } from "@anthropic-ai/sdk"
import { currentUser } from "@clerk/nextjs"
const anthropic = new Anthropic({ const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!, apiKey: process.env.ANTHROPIC_API_KEY!,
}) })
// Format file structure for context // Format file structure for context
function formatFileStructure(items: (TFile | TFolder)[] | undefined, prefix = ""): string { function formatFileStructure(
items: (TFile | TFolder)[] | undefined,
prefix = ""
): string {
if (!items || !Array.isArray(items)) { if (!items || !Array.isArray(items)) {
return "No files available" return "No files available"
} }
@ -23,15 +29,23 @@ function formatFileStructure(items: (TFile | TFolder)[] | undefined, prefix = ""
return sortedItems return sortedItems
.map((item) => { .map((item) => {
if (item.type === "file" && !ignoredFiles.some( if (
(pattern) => item.name.endsWith(pattern.replace("*", "")) || item.name === pattern item.type === "file" &&
)) { !ignoredFiles.some(
(pattern) =>
item.name.endsWith(pattern.replace("*", "")) ||
item.name === pattern
)
) {
return `${prefix}├── ${item.name}` return `${prefix}├── ${item.name}`
} else if ( } else if (
item.type === "folder" && item.type === "folder" &&
!ignoredFolders.some((folder) => folder === item.name) !ignoredFolders.some((folder) => folder === item.name)
) { ) {
const folderContent = formatFileStructure(item.children, `${prefix}`) const folderContent = formatFileStructure(
item.children,
`${prefix}`
)
return `${prefix}├── ${item.name}/\n${folderContent}` return `${prefix}├── ${item.name}/\n${folderContent}`
} }
return null return null
@ -94,6 +108,7 @@ export async function POST(request: Request) {
line, line,
templateType, templateType,
files, files,
projectName,
} = await request.json() } = await request.json()
// Get template configuration // Get template configuration
@ -138,13 +153,23 @@ Instructions: ${messages[0].content}
Respond only with the modified code that can directly replace the existing code.` Respond only with the modified code that can directly replace the existing code.`
} else { } else {
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example: systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. When providing code:
\`\`\`python 1. Format it using triple backticks (\`\`\`) with the appropriate language identifier.
print("Hello, World!") 2. Always specify the complete file path in the format:
\`\`\` ${projectName}/filepath/to/file.ext
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point. 3. If creating a new file, specify the path as:
${projectName}/filepath/to/file.ext (new file)
4. Format your code blocks as:
${projectName}/filepath/to/file.ext
\`\`\`language
code here
\`\`\`
If multiple files are involved, repeat the format for each file. Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
This is the project template: This is the project template:
${templateContext} ${templateContext}

View File

@ -19,7 +19,9 @@ export async function POST(request: Request) {
- Explanations or comments - Explanations or comments
- Markdown formatting - Markdown formatting
The output should be the exact code that will replace the existing code, nothing more and nothing less.` The output should be the exact code that will replace the existing code, nothing more and nothing less.
Important: When merging, preserve the original code structure as much as possible. Only make necessary changes to integrate the new code while maintaining the original code's organization and style.`
const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}` const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`

View File

@ -8,7 +8,7 @@ interface ApplyButtonProps {
activeFileName: string activeFileName: string
activeFileContent: string activeFileContent: string
editorRef: { current: any } editorRef: { current: any }
onApply: (mergedCode: string) => void onApply: (mergedCode: string, originalCode: string) => void
} }
export default function ApplyButton({ export default function ApplyButton({
@ -48,7 +48,7 @@ export default function ApplyButton({
mergedCode += decoder.decode(value, { stream: true }) mergedCode += decoder.decode(value, { stream: true })
} }
} }
onApply(mergedCode.trim()) onApply(mergedCode.trim(), activeFileContent)
} catch (error) { } catch (error) {
console.error("Error applying code:", error) console.error("Error applying code:", error)
toast.error( toast.error(

View File

@ -19,6 +19,7 @@ export default function ChatMessage({
editorRef, editorRef,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection, setMergeDecorationsCollection,
selectFile,
}: MessageProps) { }: MessageProps) {
// State for expanded message index // State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState< const [expandedMessageIndex, setExpandedMessageIndex] = useState<
@ -115,8 +116,9 @@ export default function ChatMessage({
activeFileContent, activeFileContent,
editorRef, editorRef,
handleApplyCode, handleApplyCode,
selectFile,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection setMergeDecorationsCollection,
) )
return ( return (

View File

@ -19,8 +19,10 @@ export default function AIChat({
files, files,
templateType, templateType,
handleApplyCode, handleApplyCode,
selectFile,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection, setMergeDecorationsCollection,
projectName,
}: AIChatProps) { }: AIChatProps) {
// Initialize socket and messages // Initialize socket and messages
const { socket } = useSocket() const { socket } = useSocket()
@ -152,7 +154,8 @@ export default function AIChat({
activeFileContent, activeFileContent,
false, false,
templateType, templateType,
files files,
projectName
) )
} }
@ -223,6 +226,7 @@ export default function AIChat({
editorRef={editorRef} editorRef={editorRef}
mergeDecorationsCollection={mergeDecorationsCollection} mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection} setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
))} ))}
{isLoading && <LoadingDots />} {isLoading && <LoadingDots />}

View File

@ -1,4 +1,4 @@
import { TFolder, TFile } from "@/lib/types" import { TFile, TFolder } from "@/lib/types"
import React from "react" import React from "react"
// Stringify content for chat message component // Stringify content for chat message component
@ -93,7 +93,8 @@ export const handleSend = async (
activeFileContent: string, activeFileContent: string,
isEditMode: boolean = false, isEditMode: boolean = false,
templateType: string, templateType: string,
files: (TFile | TFolder)[] files: (TFile | TFolder)[],
projectName: string
) => { ) => {
// Return if input is empty and context is null // Return if input is empty and context is null
if (input.trim() === "" && !context) return if (input.trim() === "" && !context) return
@ -144,7 +145,8 @@ export const handleSend = async (
activeFileContent: activeFileContent, activeFileContent: activeFileContent,
isEditMode: isEditMode, isEditMode: isEditMode,
templateType: templateType, templateType: templateType,
files: files, files: files,
projectName: projectName,
}), }),
signal: abortControllerRef.current.signal, signal: abortControllerRef.current.signal,
}) })
@ -241,3 +243,11 @@ export const looksLikeCode = (text: string): boolean => {
return codeIndicators.some((pattern) => pattern.test(text)) return codeIndicators.some((pattern) => pattern.test(text))
} }
// Add this new function after looksLikeCode function
export const isFilePath = (text: string): boolean => {
// Match patterns like next/styles/SignIn.module.css or path/to/file.ext (new file)
const pattern =
/^(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/
return pattern.test(text)
}

View File

@ -1,11 +1,13 @@
import { Check, CornerUpLeft, X } from "lucide-react" import { useSocket } from "@/context/SocketContext"
import { TTab } from "@/lib/types"
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
import monaco from "monaco-editor" import monaco from "monaco-editor"
import { Components } from "react-markdown" import { Components } from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
import { Button } from "../../../ui/button" import { Button } from "../../../ui/button"
import ApplyButton from "../ApplyButton" import ApplyButton from "../ApplyButton"
import { stringifyContent } from "./chatUtils" import { isFilePath, stringifyContent } from "./chatUtils"
// Create markdown components for chat message component // Create markdown components for chat message component
export const createMarkdownComponents = ( export const createMarkdownComponents = (
@ -15,7 +17,8 @@ export const createMarkdownComponents = (
activeFileName: string, activeFileName: string,
activeFileContent: string, activeFileContent: string,
editorRef: any, editorRef: any,
handleApplyCode: (mergedCode: string) => void, handleApplyCode: (mergedCode: string, originalCode: string) => void,
selectFile: (tab: TTab) => void,
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection, mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
): Components => ({ ): Components => ({
@ -58,7 +61,7 @@ export const createMarkdownComponents = (
mergeDecorationsCollection && mergeDecorationsCollection &&
editorRef?.current editorRef?.current
) { ) {
mergeDecorationsCollection.clear() mergeDecorationsCollection?.clear()
setMergeDecorationsCollection(undefined) setMergeDecorationsCollection(undefined)
} }
}} }}
@ -72,14 +75,15 @@ export const createMarkdownComponents = (
<div className="w-px bg-input"></div> <div className="w-px bg-input"></div>
<Button <Button
onClick={() => { onClick={() => {
if ( if (editorRef?.current && mergeDecorationsCollection) {
setMergeDecorationsCollection && const model = editorRef.current.getModel()
mergeDecorationsCollection && if (model && (model as any).originalContent) {
editorRef?.current editorRef.current?.setValue(
) { (model as any).originalContent
editorRef.current.getModel()?.setValue(activeFileContent) )
mergeDecorationsCollection.clear() mergeDecorationsCollection.clear()
setMergeDecorationsCollection(undefined) setMergeDecorationsCollection?.(undefined)
}
} }
}} }}
size="sm" size="sm"
@ -126,8 +130,62 @@ export const createMarkdownComponents = (
) )
}, },
// Render markdown elements // Render markdown elements
p: ({ node, children, ...props }) => p: ({ node, children, ...props }) => {
renderMarkdownElement({ node, children, ...props }), const content = stringifyContent(children)
const { socket } = useSocket()
if (isFilePath(content)) {
const isNewFile = content.endsWith("(new file)")
const filePath = (
isNewFile ? content.replace(" (new file)", "") : content
)
.split("/")
.filter((part, index) => index !== 0)
.join("/")
const handleFileClick = () => {
if (isNewFile) {
socket?.emit(
"createFile",
{
name: filePath,
},
(response: any) => {
if (response.success) {
const tab: TTab = {
id: filePath,
name: filePath.split("/").pop() || "",
saved: true,
type: "file",
}
selectFile(tab)
}
}
)
} else {
const tab: TTab = {
id: filePath,
name: filePath.split("/").pop() || "",
saved: true,
type: "file",
}
selectFile(tab)
}
}
return (
<div
onClick={handleFileClick}
className="group flex items-center gap-2 px-2 py-1 bg-secondary/50 rounded-md my-2 text-xs hover:bg-secondary cursor-pointer w-fit"
>
<FileText className="h-4 w-4" />
<span className="font-mono group-hover:underline">{content}</span>
</div>
)
}
return renderMarkdownElement({ node, children, ...props })
},
h1: ({ node, children, ...props }) => h1: ({ node, children, ...props }) =>
renderMarkdownElement({ node, children, ...props }), renderMarkdownElement({ node, children, ...props }),
h2: ({ node, children, ...props }) => h2: ({ node, children, ...props }) =>

View File

@ -1,5 +1,5 @@
import { TemplateConfig } from "@/lib/templates" import { TemplateConfig } from "@/lib/templates"
import { TFile, TFolder } from "@/lib/types" import { TFile, TFolder, TTab } from "@/lib/types"
import * as monaco from "monaco-editor" import * as monaco from "monaco-editor"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
@ -56,9 +56,11 @@ export interface AIChatProps {
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
templateType: string templateType: string
templateConfig?: TemplateConfig templateConfig?: TemplateConfig
handleApplyCode: (mergedCode: string) => void projectName: string
handleApplyCode: (mergedCode: string, originalCode: string) => void
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Chat input props interface // Chat input props interface
@ -108,12 +110,13 @@ export interface MessageProps {
) => void ) => void
setIsContextExpanded: (isExpanded: boolean) => void setIsContextExpanded: (isExpanded: boolean) => void
socket: Socket | null socket: Socket | null
handleApplyCode: (mergedCode: string) => void handleApplyCode: (mergedCode: string, originalCode: string) => void
activeFileName: string activeFileName: string
activeFileContent: string activeFileContent: string
editorRef: any editorRef: any
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Context tabs props interface // Context tabs props interface

View File

@ -384,10 +384,10 @@ export default function CodeEditor({
// handle apply code // handle apply code
const handleApplyCode = useCallback( const handleApplyCode = useCallback(
(mergedCode: string) => { (mergedCode: string, originalCode: string) => {
if (!editorRef) return if (!editorRef) return
const originalLines = activeFileContent.split("\n") const originalLines = originalCode.split("\n")
const mergedLines = mergedCode.split("\n") const mergedLines = mergedCode.split("\n")
const decorations: monaco.editor.IModelDeltaDecoration[] = [] const decorations: monaco.editor.IModelDeltaDecoration[] = []
@ -397,32 +397,31 @@ export default function CodeEditor({
i < Math.max(originalLines.length, mergedLines.length); i < Math.max(originalLines.length, mergedLines.length);
i++ i++
) { ) {
if (originalLines[i] !== mergedLines[i]) { // Only highlight new lines (green highlights)
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
decorations.push({ decorations.push({
range: new monaco.Range(i + 1, 1, i + 1, 1), range: new monaco.Range(i + 1, 1, i + 1, 1),
options: { options: {
isWholeLine: true, isWholeLine: true,
className: className: "added-line-decoration",
i < originalLines.length glyphMarginClassName: "added-line-glyph",
? "removed-line-decoration"
: "added-line-decoration",
glyphMarginClassName:
i < originalLines.length
? "removed-line-glyph"
: "added-line-glyph",
}, },
}) })
} }
} }
// Update editor content // Store original content in the model
const model = editorRef.getModel()
if (model) {
;(model as any).originalContent = originalCode
}
editorRef.setValue(mergedCode) editorRef.setValue(mergedCode)
// Apply decorations // Apply decorations
const newDecorations = editorRef.createDecorationsCollection(decorations) const newDecorations = editorRef.createDecorationsCollection(decorations)
setMergeDecorationsCollection(newDecorations) setMergeDecorationsCollection(newDecorations)
}, },
[editorRef, activeFileContent] [editorRef]
) )
// Generate widget effect // Generate widget effect
@ -1284,9 +1283,11 @@ export default function CodeEditor({
lastCopiedRangeRef={lastCopiedRangeRef} lastCopiedRangeRef={lastCopiedRangeRef}
files={files} files={files}
templateType={sandboxData.type} templateType={sandboxData.type}
projectName={sandboxData.name}
handleApplyCode={handleApplyCode} handleApplyCode={handleApplyCode}
mergeDecorationsCollection={mergeDecorationsCollection} mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection} setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
</ResizablePanel> </ResizablePanel>
</> </>

View File

@ -9,7 +9,7 @@ import {
import { useTerminal } from "@/context/TerminalContext" import { useTerminal } from "@/context/TerminalContext"
import { Sandbox, User } from "@/lib/types" import { Sandbox, User } from "@/lib/types"
import { Globe } from "lucide-react" import { Globe } from "lucide-react"
import { useEffect, useState } from "react" import { useState } from "react"
export default function DeployButtonModal({ export default function DeployButtonModal({
userData, userData,
@ -18,19 +18,8 @@ export default function DeployButtonModal({
userData: User userData: User
data: Sandbox data: Sandbox
}) { }) {
const { deploy, getAppExists } = useTerminal() const { deploy } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false) const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false)
useEffect(() => {
const checkDeployment = async () => {
if (getAppExists) {
const exists = await getAppExists(data.id)
setIsDeployed(exists)
}
}
checkDeployment()
}, [data.id, getAppExists])
const handleDeploy = () => { const handleDeploy = () => {
if (isDeploying) { if (isDeploying) {
@ -41,7 +30,6 @@ export default function DeployButtonModal({
setIsDeploying(true) setIsDeploying(true)
deploy(() => { deploy(() => {
setIsDeploying(false) setIsDeploying(false)
setIsDeployed(true)
}) })
} }
} }
@ -64,9 +52,8 @@ export default function DeployButtonModal({
<DeploymentOption <DeploymentOption
icon={<Globe className="text-gray-500 w-5 h-5" />} icon={<Globe className="text-gray-500 w-5 h-5" />}
domain={`${data.id}.gitwit.app`} domain={`${data.id}.gitwit.app`}
timestamp="Deployed 1m ago" timestamp="Deployed 1h ago"
user={userData.name} user={userData.name}
isDeployed={isDeployed}
/> />
</div> </div>
<Button <Button
@ -74,7 +61,7 @@ export default function DeployButtonModal({
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
onClick={handleDeploy} onClick={handleDeploy}
> >
{isDeploying ? "Deploying..." : isDeployed ? "Update" : "Deploy"} {isDeploying ? "Deploying..." : "Update"}
</Button> </Button>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@ -87,33 +74,27 @@ function DeploymentOption({
domain, domain,
timestamp, timestamp,
user, user,
isDeployed,
}: { }: {
icon: React.ReactNode icon: React.ReactNode
domain: string domain: string
timestamp: string timestamp: string
user: string user: string
isDeployed: boolean
}) { }) {
return ( return (
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900"> <div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
<div className="flex items-start gap-2 relative"> <div className="flex items-start gap-2 relative">
<div className="flex-shrink-0">{icon}</div> <div className="flex-shrink-0">{icon}</div>
{isDeployed ? ( <a
<a href={`https://${domain}`}
href={`https://${domain}`} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="font-semibold text-gray-300 hover:underline"
className="font-semibold text-gray-300 hover:underline" >
> {domain}
{domain} </a>
</a>
) : (
<span className="font-semibold text-gray-300">{domain}</span>
)}
</div> </div>
<p className="text-sm text-gray-400 mt-0 ml-7"> <p className="text-sm text-gray-400 mt-0 ml-7">
{isDeployed ? `${timestamp}${user}` : "Never deployed"} {timestamp} {user}
</p> </p>
</div> </div>
) )

View File

@ -20,7 +20,6 @@ interface TerminalContextType {
createNewTerminal: (command?: string) => Promise<void> createNewTerminal: (command?: string) => Promise<void>
closeTerminal: (id: string) => void closeTerminal: (id: string) => void
deploy: (callback: () => void) => void deploy: (callback: () => void) => void
getAppExists: ((appName: string) => Promise<boolean>) | null
} }
const TerminalContext = createContext<TerminalContextType | undefined>( const TerminalContext = createContext<TerminalContextType | undefined>(
@ -36,19 +35,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
>([]) >([])
const [activeTerminalId, setActiveTerminalId] = useState<string>("") const [activeTerminalId, setActiveTerminalId] = useState<string>("")
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false) const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
const [isSocketReady, setIsSocketReady] = useState<boolean>(false)
// Listen for the "ready" signal from the socket
React.useEffect(() => {
if (socket) {
socket.on("ready", () => {
setIsSocketReady(true)
})
}
return () => {
if (socket) socket.off("ready")
}
}, [socket])
const createNewTerminal = async (command?: string): Promise<void> => { const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return if (!socket) return
@ -92,18 +78,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
}) })
} }
const getAppExists = async (appName: string): Promise<boolean> => {
console.log("Is there a socket: " + !!socket)
if (!socket) {
console.error("Couldn't check if app exists: No socket")
return false
}
const response: { exists: boolean } = await new Promise((resolve) =>
socket.emit("getAppExists", { appName }, resolve)
)
return response.exists
}
const value = { const value = {
terminals, terminals,
setTerminals, setTerminals,
@ -114,7 +88,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
deploy, deploy,
getAppExists: isSocketReady ? getAppExists : null,
} }
return ( return (