import React, { useEffect, useState, useRef } from "react";
import { Editor, DiffEditor } from "@monaco-editor/react";
import { create } from "zustand";
import { io } from "socket.io-client";
const BASE_URL = "http://localhost:7860";
const useStore = create((set, get) => ({
files: [],
selectedFile: null,
editorContent: "",
dbSchema: { tables: [] },
chat: [],
consoleOutput: "",
loading: false,
fileProposals: {},
showDiffForFile: null,
fileContents: {},
setFiles: (files) => set({ files }),
setSelectedFile: (name) => set({ selectedFile: { name } }),
setEditorContent: (editorContent) => set({ editorContent }),
setDbSchema: (dbSchema) => set({ dbSchema }),
pushChat: (msg) => set((s) => ({ chat: [...s.chat, msg] })),
setConsoleOutput: (consoleOutput) => set({ consoleOutput }),
setLoading: (loading) => set({ loading }),
setFileProposal: (name, details) => set((s) => ({ fileProposals: { ...s.fileProposals, [name]: { ...details, active: true } } })),
setShowDiffForFile: (fileName) => set({ showDiffForFile: fileName }),
openFile: async (name) => {
const currentState = get();
if (currentState.selectedFile?.name && currentState.editorContent) {
set({ fileContents: { ...currentState.fileContents, [currentState.selectedFile.name]: currentState.editorContent } });
}
set({ loading: true });
let content = "";
const state = get();
const proposal = state.fileProposals[name];
if (proposal?.isNew) {
content = "";
} else {
content = state.fileContents[name];
if (!content) {
try {
const res = await fetch(`${BASE_URL}/files/${encodeURIComponent(name)}`);
if (!res.ok) throw new Error("Failed to load file");
const data = await res.json();
content = data.content;
set({ fileContents: { ...get().fileContents, [name]: content } });
} catch (e) {
console.warn(e);
content = `-- demo content for ${name}\nSELECT 1;`;
set({ fileContents: { ...get().fileContents, [name]: content } });
}
}
}
set({ selectedFile: { name }, editorContent: content, loading: false });
const finalState = get();
const prop = finalState.fileProposals[name];
if (prop?.active) {
set({ showDiffForFile: name });
} else {
set({ showDiffForFile: null });
}
},
acceptProposal: () => {
const state = get();
const file = state.selectedFile?.name;
if (state.showDiffForFile !== file) return;
const prop = state.fileProposals[file];
if (!prop) return;
if (prop.isNew) {
const confirmedName = prompt("Save new file as (.sql required):", prop.suggestedName);
if (!confirmedName?.trim() || !confirmedName.endsWith(".sql")) {
return;
}
const finalName = confirmedName.trim();
fetch(`${BASE_URL}/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: finalName, content: prop.code }),
})
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
})
.then(() => {
const newProps = { ...state.fileProposals };
delete newProps[file];
const currentFiles = get().files;
set({
files: [...currentFiles, { name: finalName }],
editorContent: prop.code,
fileContents: { ...get().fileContents, [finalName]: prop.code },
showDiffForFile: null,
fileProposals: newProps,
});
if (finalName !== file) {
set({ selectedFile: { name: finalName } });
}
})
.catch((e) => console.error("Failed to create file:", e));
} else {
fetch(`${BASE_URL}/files/${encodeURIComponent(file)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: prop.code }),
})
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
const newProps = { ...state.fileProposals };
delete newProps[file];
set({
editorContent: prop.code,
fileContents: { ...state.fileContents, [file]: prop.code },
showDiffForFile: null,
fileProposals: newProps,
});
})
.catch((e) => console.error("Failed to save after accept:", e));
}
},
rejectProposal: () => {
const state = get();
const file = state.selectedFile?.name;
if (state.showDiffForFile !== file) return;
const newProps = { ...state.fileProposals };
delete newProps[file];
set({ showDiffForFile: null, fileProposals: newProps });
},
refreshFiles: async () => {
try {
const res = await fetch(`${BASE_URL}/files`);
if (!res.ok) throw new Error("Failed to load files");
let data = await res.json();
data = data.filter((f) => f.name.endsWith(".sql"));
set({ files: data || [] });
} catch (e) {
console.warn(e);
}
},
}));
const Card = ({ children, className = "" }) => (
{children}
);
const CardContent = ({ children, className = "" }) => (
{children}
);
const IconButton = ({ children, onClick, className = "" }) => (
);
function FileExplorer() {
const { files, selectedFile, loading, fileProposals, fileContents, editorContent } = useStore();
const openFile = useStore((s) => s.openFile);
const loadFiles = async () => {
useStore.setState({ loading: true });
try {
const res = await fetch(`${BASE_URL}/files`);
if (!res.ok) throw new Error("Failed to load files");
let data = await res.json();
data = data.filter((f) => f.name.endsWith(".sql"));
useStore.setState({ files: data || [] });
} catch (e) {
console.warn(e);
} finally {
useStore.setState({ loading: false });
}
};
useEffect(() => {
loadFiles();
}, []);
useEffect(() => {
if (files.length > 0 && !selectedFile?.name) {
openFile(files[0].name);
}
}, [files, selectedFile?.name, openFile]);
async function createNewFile() {
const name = prompt("New file name (must end with .sql):");
if (!name || !name.endsWith(".sql")) return;
try {
await fetch(`${BASE_URL}/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: name, content: "" }),
});
loadFiles();
} catch (e) {
console.warn(e);
}
}
const proposedNews = Object.entries(fileProposals).filter(([n, p]) => p.isNew);
return (
Files
New
{files.map((f) => {
const hasProposal = fileProposals[f.name]?.active;
const isDirty = f.name === selectedFile?.name && (!fileContents[f.name] || editorContent !== fileContents[f.name]);
return (
-
);
})}
{proposedNews.map(([name]) => {
const prop = fileProposals[name];
const isDirty = name === selectedFile?.name && (!fileContents[name] || editorContent !== fileContents[name]);
return (
-
);
})}
App Status: {loading ? "loading" : "ready"}
);
}
function DBViewer() {
const { dbSchema, setDbSchema, setConsoleOutput, setFileProposal, setShowDiffForFile} = useStore();
useEffect(() => {
async function loadSchema() {
try {
const tres = await fetch(`${BASE_URL}/tables`);
if (!tres.ok) throw new Error("no tables");
const tables = await tres.json();
const tablesWithCols = await Promise.all(
tables.map(async (t) => {
const qres = await fetch(`${BASE_URL}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: `PRAGMA table_info(${t})` }),
});
const colsData = await qres.json();
console.log(colsData)
const columns = colsData.map((c) => `${c.name} ${c.type}`);
return { name: t, columns };
})
);
setDbSchema({ tables: tables.map((t) => ({ name: t, columns: [] })) });
} catch (e) {
}
}
loadSchema();
}, [setDbSchema]);
async function inspectTable(table) {
try {
const res = await fetch(`${BASE_URL}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: `SELECT * FROM ${table} LIMIT 50;` }),
});
const data = await res.json();
setConsoleOutput(JSON.stringify(data, null, 2));
} catch (e) {
setConsoleOutput(`Failed to query table ${table}: ${e.message}`);
}
}
return (
{dbSchema.tables.map((t) => (
{t.name}
{t.columns.length} columns
inspectTable(t.name)}>Preview
))}
);
}
function WSListener() {
const { setFileProposal, setShowDiffForFile, selectedFile, pushChat } = useStore();
const [isWaiting, setIsWaiting] = useState(true);
useEffect(() => {
const socket = io("http://localhost:7860");
socket.on("connect", () => {
console.log("Connected to WebSocket server");
setIsWaiting(false);
});
socket.on("event", (data) => {
console.log("WS message:", data);
if (data.kind === "code_change") {
setFileProposal(data.filename || selectedFile?.name, {
code: data.proposedCode,
isNew: data.isNew,
suggestedName: data.newFileName,
});
setShowDiffForFile(data.filename || selectedFile?.name);
}
});
socket.on("log", (data) => {
console.log("WS message:", data);
pushChat({ role: "agent", text: data.msg.replace(/\u001b\[[0-9;]*m/g, '') });
});
socket.on("disconnect", () => {
console.log("Disconnected from WebSocket server");
setIsWaiting(true);
});
return () => {
socket.disconnect();
};
}, []);
return isWaiting ? Waiting for agent response...
: null;
}
function AgentChat() {
const { chat, pushChat, setFileProposal, setShowDiffForFile, selectedFile, openFile } = useStore();
const [input, setInput] = useState("");
const [sending, setSending] = useState(false);
async function sendMessage() {
if (!input.trim() || sending) return;
pushChat({ role: "user", text: input });
setInput("");
setSending(true);
try {
const res = await fetch(`${BASE_URL}/run_crew`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ instructions: input, file: selectedFile?.name }),
});
const data = await res.json();
pushChat({ role: "agent", text: data.status || "Crew is Working..." });
} catch (e) {
pushChat({ role: "agent", text: `Error: ${e.message}` });
} finally {
setSending(false);
}
}
return (
Agent
{chat.map((m, i) => (
{m.text}
{m.proposedCode && (
)}
))}
setInput(e.target.value)}
placeholder="Instruct SQL agent"
onKeyDown={(e) => {
if (e.key === "Enter") sendMessage();
}}
/>
{sending ? "..." : "Send"}
);
}
function OutputConsole() {
const { consoleOutput } = useStore();
return (
{consoleOutput || "Console output will appear here."}
);
}
function SQLMonacoEditor() {
const {
editorContent,
setEditorContent,
selectedFile,
showDiffForFile,
fileProposals,
fileContents,
acceptProposal,
rejectProposal,
} = useStore();
const proposal = fileProposals[selectedFile?.name];
const showDiff = !!proposal?.active && showDiffForFile === selectedFile?.name;
const proposedContent = proposal?.code || "";
const isDirty = selectedFile?.name && (!fileContents[selectedFile.name] || editorContent !== fileContents[selectedFile.name]);
const saveFile = async () => {
let fileName = selectedFile?.name;
if (!fileName) {
const name = prompt("Save as (must end with .sql):");
if (!name || !name.endsWith(".sql")) {
alert("Invalid filename");
return;
}
fileName = name;
try {
const res = await fetch(`${BASE_URL}/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: name, content: editorContent }),
});
if (!res.ok) throw new Error("Create failed");
const currentFiles = useStore.getState().files;
useStore.setState({ files: [...currentFiles, { name }] });
useStore.setState({ selectedFile: { name }, editorContent, fileContents: { ...useStore.getState().fileContents, [name]: editorContent } });
alert("File created and saved");
return;
} catch (e) {
alert("Failed to create file: " + e.message);
return;
}
}
try {
await fetch(`${BASE_URL}/files/${encodeURIComponent(fileName)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: editorContent }),
});
useStore.setState({ fileContents: { ...useStore.getState().fileContents, [fileName]: editorContent } });
alert("Saved");
} catch (e) {
alert("Save failed: " + e.message);
}
};
const runQuery = async () => {
let fileName = selectedFile?.name;
if (!fileName) {
const name = prompt("Save query as (must end with .sql):");
if (!name || !name.endsWith(".sql")) {
alert("Invalid filename");
return;
}
fileName = name;
try {
const res = await fetch(`${BASE_URL}/files`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: name, content: editorContent }),
});
if (!res.ok) throw new Error("Create failed");
const currentFiles = useStore.getState().files;
useStore.setState({ files: [...currentFiles, { name }], selectedFile: { name }, editorContent, fileContents: { ...useStore.getState().fileContents, [name]: editorContent } });
} catch (e) {
alert("Failed to create file: " + e.message);
return;
}
}
try {
const res = await fetch(`${BASE_URL}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: editorContent }),
});
if (!res.ok) {
let err;
try {
err = await res.json();
} catch {
err = "Query failed";
}
throw new Error(err.error || JSON.stringify(err) || err);
}
const data = await res.json();
useStore.setState({ consoleOutput: JSON.stringify(data, null, 2) });
} catch (e) {
useStore.setState({ consoleOutput: "Run failed: " + e.message });
}
};
const commonOptions = { minimap: { enabled: false }, fontSize: 13, wordWrap: "on" };
const editorKey = `${selectedFile?.name}-${showDiff ? "diff" : "normal"}`;
return (
{selectedFile?.name || "No file selected"}{isDirty ? " *" : ""}
SQL Editor {showDiff ? "(Diff View)" : ""}
{showDiff ? (
<>
Accept
Reject
>
) : (
<>
Save
Run
>
)}
{showDiff ? (
) : (
setEditorContent(v || "")}
options={commonOptions}
/>
)}
);
}
export default function App() {
return (
);
}