Compare commits
10 Commits
feature/ap
...
deployment
Author | SHA1 | Date | |
---|---|---|---|
a59246b1ef | |||
1630a5a9cd | |||
ffdcdef56c | |||
6612692d98 | |||
534b148b86 | |||
e384607d24 | |||
e7d9989931 | |||
42305d67b9 | |||
06dadf3a0b | |||
ba7a1dcc2c |
@ -22,6 +22,7 @@ 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.
|
||||||
|
|
||||||
@ -152,6 +153,7 @@ 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
|
||||||
@ -256,7 +258,6 @@ 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
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ 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=
|
||||||
|
@ -31,9 +31,30 @@ 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("apps:list")
|
const response = await this.sendCommand("--quiet apps:list")
|
||||||
// Split the output by newline and remove the header
|
return response.output.split("\n")
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +156,28 @@ 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")
|
||||||
@ -253,7 +275,9 @@ export class Sandbox {
|
|||||||
getFolder: handleGetFolder,
|
getFolder: handleGetFolder,
|
||||||
saveFile: handleSaveFile,
|
saveFile: handleSaveFile,
|
||||||
moveFile: handleMoveFile,
|
moveFile: handleMoveFile,
|
||||||
list: handleListApps,
|
listApps: handleListApps,
|
||||||
|
getAppCreatedAt: handleGetAppCreatedAt,
|
||||||
|
getAppExists: handleAppExists,
|
||||||
deploy: handleDeploy,
|
deploy: handleDeploy,
|
||||||
createFile: handleCreateFile,
|
createFile: handleCreateFile,
|
||||||
createFolder: handleCreateFolder,
|
createFolder: handleCreateFolder,
|
||||||
|
@ -146,6 +146,8 @@ io.on("connection", async (socket) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.emit("ready")
|
||||||
|
|
||||||
// Handle disconnection event
|
// Handle disconnection event
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -3,21 +3,18 @@ CLERK_SECRET_KEY=
|
|||||||
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
||||||
LIVEBLOCKS_SECRET_KEY=
|
LIVEBLOCKS_SECRET_KEY=
|
||||||
|
|
||||||
NEXT_PUBLIC_SERVER_PORT=4000
|
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
|
||||||
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
|
||||||
# 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.
|
||||||
NEXT_PUBLIC_DATABASE_WORKER_URL=https://database.your-worker.workers.dev
|
# These URLs should begin with https:// in production
|
||||||
NEXT_PUBLIC_STORAGE_WORKER_URL=https://storage.your-worker.workers.dev
|
NEXT_PUBLIC_DATABASE_WORKER_URL=
|
||||||
NEXT_PUBLIC_WORKERS_KEY=SUPERDUPERSECRET
|
NEXT_PUBLIC_STORAGE_WORKER_URL=
|
||||||
|
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=
|
|
@ -1,22 +1,16 @@
|
|||||||
import {
|
|
||||||
ignoredFiles,
|
|
||||||
ignoredFolders,
|
|
||||||
} from "@/components/editor/AIChat/lib/ignored-paths"
|
|
||||||
import { templateConfigs } from "@/lib/templates"
|
|
||||||
import { TIERS } from "@/lib/tiers"
|
|
||||||
import { TFile, TFolder } from "@/lib/types"
|
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs"
|
||||||
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import { TIERS } from "@/lib/tiers"
|
||||||
|
import { templateConfigs } from "@/lib/templates"
|
||||||
|
import { TFile, TFolder } from "@/lib/types"
|
||||||
|
import { ignoredFiles, ignoredFolders } from "@/components/editor/AIChat/lib/ignored-paths"
|
||||||
|
|
||||||
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(
|
function formatFileStructure(items: (TFile | TFolder)[] | undefined, prefix = ""): string {
|
||||||
items: (TFile | TFolder)[] | undefined,
|
|
||||||
prefix = ""
|
|
||||||
): string {
|
|
||||||
if (!items || !Array.isArray(items)) {
|
if (!items || !Array.isArray(items)) {
|
||||||
return "No files available"
|
return "No files available"
|
||||||
}
|
}
|
||||||
@ -29,23 +23,15 @@ function formatFileStructure(
|
|||||||
|
|
||||||
return sortedItems
|
return sortedItems
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (
|
if (item.type === "file" && !ignoredFiles.some(
|
||||||
item.type === "file" &&
|
(pattern) => item.name.endsWith(pattern.replace("*", "")) || item.name === pattern
|
||||||
!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(
|
const folderContent = formatFileStructure(item.children, `${prefix}│ `)
|
||||||
item.children,
|
|
||||||
`${prefix}│ `
|
|
||||||
)
|
|
||||||
return `${prefix}├── ${item.name}/\n${folderContent}`
|
return `${prefix}├── ${item.name}/\n${folderContent}`
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -108,7 +94,6 @@ 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
|
||||||
@ -153,23 +138,13 @@ 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. When providing code:
|
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:
|
||||||
|
|
||||||
1. Format it using triple backticks (\`\`\`) with the appropriate language identifier.
|
\`\`\`python
|
||||||
2. Always specify the complete file path in the format:
|
print("Hello, World!")
|
||||||
${projectName}/filepath/to/file.ext
|
\`\`\`
|
||||||
|
|
||||||
3. If creating a new file, specify the path as:
|
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||||
${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}
|
||||||
|
@ -19,9 +19,7 @@ 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}`
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ interface ApplyButtonProps {
|
|||||||
activeFileName: string
|
activeFileName: string
|
||||||
activeFileContent: string
|
activeFileContent: string
|
||||||
editorRef: { current: any }
|
editorRef: { current: any }
|
||||||
onApply: (mergedCode: string, originalCode: string) => void
|
onApply: (mergedCode: 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(), activeFileContent)
|
onApply(mergedCode.trim())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error applying code:", error)
|
console.error("Error applying code:", error)
|
||||||
toast.error(
|
toast.error(
|
||||||
|
@ -19,7 +19,6 @@ 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<
|
||||||
@ -116,9 +115,8 @@ export default function ChatMessage({
|
|||||||
activeFileContent,
|
activeFileContent,
|
||||||
editorRef,
|
editorRef,
|
||||||
handleApplyCode,
|
handleApplyCode,
|
||||||
selectFile,
|
|
||||||
mergeDecorationsCollection,
|
mergeDecorationsCollection,
|
||||||
setMergeDecorationsCollection,
|
setMergeDecorationsCollection
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,10 +19,8 @@ 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()
|
||||||
@ -154,8 +152,7 @@ export default function AIChat({
|
|||||||
activeFileContent,
|
activeFileContent,
|
||||||
false,
|
false,
|
||||||
templateType,
|
templateType,
|
||||||
files,
|
files
|
||||||
projectName
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +223,6 @@ export default function AIChat({
|
|||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
mergeDecorationsCollection={mergeDecorationsCollection}
|
||||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
||||||
selectFile={selectFile}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isLoading && <LoadingDots />}
|
{isLoading && <LoadingDots />}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TFile, TFolder } from "@/lib/types"
|
import { TFolder, TFile } from "@/lib/types"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
// Stringify content for chat message component
|
// Stringify content for chat message component
|
||||||
@ -93,8 +93,7 @@ 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
|
||||||
@ -145,8 +144,7 @@ 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,
|
||||||
})
|
})
|
||||||
@ -243,11 +241,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { useSocket } from "@/context/SocketContext"
|
import { Check, CornerUpLeft, X } from "lucide-react"
|
||||||
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 { isFilePath, stringifyContent } from "./chatUtils"
|
import { stringifyContent } from "./chatUtils"
|
||||||
|
|
||||||
// Create markdown components for chat message component
|
// Create markdown components for chat message component
|
||||||
export const createMarkdownComponents = (
|
export const createMarkdownComponents = (
|
||||||
@ -17,8 +15,7 @@ export const createMarkdownComponents = (
|
|||||||
activeFileName: string,
|
activeFileName: string,
|
||||||
activeFileContent: string,
|
activeFileContent: string,
|
||||||
editorRef: any,
|
editorRef: any,
|
||||||
handleApplyCode: (mergedCode: string, originalCode: string) => void,
|
handleApplyCode: (mergedCode: string) => void,
|
||||||
selectFile: (tab: TTab) => void,
|
|
||||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
|
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
|
||||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||||
): Components => ({
|
): Components => ({
|
||||||
@ -61,7 +58,7 @@ export const createMarkdownComponents = (
|
|||||||
mergeDecorationsCollection &&
|
mergeDecorationsCollection &&
|
||||||
editorRef?.current
|
editorRef?.current
|
||||||
) {
|
) {
|
||||||
mergeDecorationsCollection?.clear()
|
mergeDecorationsCollection.clear()
|
||||||
setMergeDecorationsCollection(undefined)
|
setMergeDecorationsCollection(undefined)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -75,15 +72,14 @@ export const createMarkdownComponents = (
|
|||||||
<div className="w-px bg-input"></div>
|
<div className="w-px bg-input"></div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (editorRef?.current && mergeDecorationsCollection) {
|
if (
|
||||||
const model = editorRef.current.getModel()
|
setMergeDecorationsCollection &&
|
||||||
if (model && (model as any).originalContent) {
|
mergeDecorationsCollection &&
|
||||||
editorRef.current?.setValue(
|
editorRef?.current
|
||||||
(model as any).originalContent
|
) {
|
||||||
)
|
editorRef.current.getModel()?.setValue(activeFileContent)
|
||||||
mergeDecorationsCollection.clear()
|
mergeDecorationsCollection.clear()
|
||||||
setMergeDecorationsCollection?.(undefined)
|
setMergeDecorationsCollection(undefined)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -130,62 +126,8 @@ export const createMarkdownComponents = (
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Render markdown elements
|
// Render markdown elements
|
||||||
p: ({ node, children, ...props }) => {
|
p: ({ node, children, ...props }) =>
|
||||||
const content = stringifyContent(children)
|
renderMarkdownElement({ node, children, ...props }),
|
||||||
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 }) =>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TemplateConfig } from "@/lib/templates"
|
import { TemplateConfig } from "@/lib/templates"
|
||||||
import { TFile, TFolder, TTab } from "@/lib/types"
|
import { TFile, TFolder } 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,11 +56,9 @@ export interface AIChatProps {
|
|||||||
files: (TFile | TFolder)[]
|
files: (TFile | TFolder)[]
|
||||||
templateType: string
|
templateType: string
|
||||||
templateConfig?: TemplateConfig
|
templateConfig?: TemplateConfig
|
||||||
projectName: string
|
handleApplyCode: (mergedCode: string) => void
|
||||||
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
|
||||||
@ -110,13 +108,12 @@ export interface MessageProps {
|
|||||||
) => void
|
) => void
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
socket: Socket | null
|
socket: Socket | null
|
||||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
handleApplyCode: (mergedCode: 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
|
||||||
|
@ -384,10 +384,10 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// handle apply code
|
// handle apply code
|
||||||
const handleApplyCode = useCallback(
|
const handleApplyCode = useCallback(
|
||||||
(mergedCode: string, originalCode: string) => {
|
(mergedCode: string) => {
|
||||||
if (!editorRef) return
|
if (!editorRef) return
|
||||||
|
|
||||||
const originalLines = originalCode.split("\n")
|
const originalLines = activeFileContent.split("\n")
|
||||||
const mergedLines = mergedCode.split("\n")
|
const mergedLines = mergedCode.split("\n")
|
||||||
|
|
||||||
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
||||||
@ -397,31 +397,32 @@ export default function CodeEditor({
|
|||||||
i < Math.max(originalLines.length, mergedLines.length);
|
i < Math.max(originalLines.length, mergedLines.length);
|
||||||
i++
|
i++
|
||||||
) {
|
) {
|
||||||
// Only highlight new lines (green highlights)
|
if (originalLines[i] !== mergedLines[i]) {
|
||||||
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: "added-line-decoration",
|
className:
|
||||||
glyphMarginClassName: "added-line-glyph",
|
i < originalLines.length
|
||||||
|
? "removed-line-decoration"
|
||||||
|
: "added-line-decoration",
|
||||||
|
glyphMarginClassName:
|
||||||
|
i < originalLines.length
|
||||||
|
? "removed-line-glyph"
|
||||||
|
: "added-line-glyph",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original content in the model
|
// Update editor content
|
||||||
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]
|
[editorRef, activeFileContent]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate widget effect
|
// Generate widget effect
|
||||||
@ -1283,11 +1284,9 @@ 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>
|
||||||
</>
|
</>
|
||||||
|
@ -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 { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export default function DeployButtonModal({
|
export default function DeployButtonModal({
|
||||||
userData,
|
userData,
|
||||||
@ -18,8 +18,19 @@ export default function DeployButtonModal({
|
|||||||
userData: User
|
userData: User
|
||||||
data: Sandbox
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { deploy } = useTerminal()
|
const { deploy, getAppExists } = 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) {
|
||||||
@ -30,6 +41,7 @@ export default function DeployButtonModal({
|
|||||||
setIsDeploying(true)
|
setIsDeploying(true)
|
||||||
deploy(() => {
|
deploy(() => {
|
||||||
setIsDeploying(false)
|
setIsDeploying(false)
|
||||||
|
setIsDeployed(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,8 +64,9 @@ 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 1h ago"
|
timestamp="Deployed 1m ago"
|
||||||
user={userData.name}
|
user={userData.name}
|
||||||
|
isDeployed={isDeployed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -61,7 +74,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..." : "Update"}
|
{isDeploying ? "Deploying..." : isDeployed ? "Update" : "Deploy"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -74,27 +87,33 @@ 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>
|
||||||
<a
|
{isDeployed ? (
|
||||||
href={`https://${domain}`}
|
<a
|
||||||
target="_blank"
|
href={`https://${domain}`}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="font-semibold text-gray-300 hover:underline"
|
rel="noopener noreferrer"
|
||||||
>
|
className="font-semibold text-gray-300 hover:underline"
|
||||||
{domain}
|
>
|
||||||
</a>
|
{domain}
|
||||||
|
</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">
|
||||||
{timestamp} • {user}
|
{isDeployed ? `${timestamp} • ${user}` : "Never deployed"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -20,6 +20,7 @@ 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>(
|
||||||
@ -35,6 +36,19 @@ 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
|
||||||
@ -78,6 +92,18 @@ 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,
|
||||||
@ -88,6 +114,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
createNewTerminal,
|
createNewTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
deploy,
|
deploy,
|
||||||
|
getAppExists: isSocketReady ? getAppExists : null,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user