Skip to content

Commit c92079c

Browse files
Upgrade workspace, AI, profile, and execution flow
1 parent a46adfb commit c92079c

22 files changed

Lines changed: 1998 additions & 343 deletions

apps/api/src/controllers/assistantController.ts

Lines changed: 91 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,136 +6,166 @@ type ChatMessage = {
66
};
77

88
type AssistantContext = {
9+
activeFileName?: string;
10+
activeFilePath?: string;
911
currentLanguage?: string;
1012
fileCount?: number;
13+
folderCount?: number;
1114
lastOutput?: string;
12-
roomId?: string;
15+
lastStatus?: string;
16+
profileName?: string;
17+
};
18+
19+
const delay = (value: number) => new Promise((resolve) => setTimeout(resolve, value));
20+
21+
const emitEvent = (res: Response, type: string, payload: unknown) => {
22+
res.write(`event: ${type}\n`);
23+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
1324
};
1425

1526
const buildReply = (message: string, context: AssistantContext) => {
1627
const normalized = message.toLowerCase();
17-
const languageHint = context.currentLanguage
18-
? ` for your current ${context.currentLanguage} workspace`
28+
const name = context.profileName ?? "builder";
29+
const fileHint = context.activeFileName
30+
? ` The active file is ${context.activeFileName}${context.activeFilePath ? ` at ${context.activeFilePath}` : ""}.`
1931
: "";
32+
const workspaceHint = ` Your workspace currently has ${context.fileCount ?? 0} files and ${context.folderCount ?? 0} folders.`;
2033

2134
if (!message.trim()) {
2235
return {
2336
reply:
24-
"Ask me anything about using VoidLAB, fixing code execution, collaborating in a room, or finding the right workflow for your current project.",
37+
`I’m the VoidLAB basic assistant for ${name}. Ask me about running code, fixing compile issues, using stdin, AI workflow, folders, or workspace commands.${workspaceHint}`,
2538
suggestions: [
2639
"How do I run code with input?",
27-
"How do collaboration rooms work?",
28-
"Give me keyboard shortcuts",
40+
"How do workspace commands work?",
41+
"How do I organize files and folders?",
2942
],
3043
};
3144
}
3245

33-
if (normalized.includes("input") || normalized.includes("stdin")) {
46+
if (normalized.includes("python")) {
3447
return {
35-
reply: `To run input/output programs${languageHint}, open the terminal input box, type the input exactly as the program expects it, and then press Run. Multi-line input is supported, so each new line will be passed to the runtime the same way it would in a local terminal.`,
48+
reply:
49+
`Python works well in VoidLAB. Choose the Python language for the active file, write your script, put stdin in the input panel if your code uses input(), and run it. If the result is wrong, compare stdout, stderr, and compile status separately because VoidLAB now shows each section distinctly.${fileHint}`,
3650
suggestions: [
37-
"Show me an example input format",
38-
"Why is my program waiting for input?",
39-
"How do I preview HTML output?",
51+
"Show a Python input example",
52+
"How do I debug Python stderr?",
53+
"Can I create multiple Python files?",
4054
],
4155
};
4256
}
4357

44-
if (
45-
normalized.includes("run") ||
46-
normalized.includes("compile") ||
47-
normalized.includes("error")
48-
) {
49-
const outputHint = context.lastOutput
50-
? ` The latest terminal response I know about is: "${context.lastOutput.slice(0, 120)}".`
51-
: "";
52-
58+
if (normalized.includes("input") || normalized.includes("stdin")) {
5359
return {
54-
reply: `Start by confirming the selected language matches the active file extension, then run the file again. If the terminal shows an error, read the first error line before changing the code because it usually points to the exact syntax or input issue.${outputHint}`,
60+
reply:
61+
`For precise stdin handling, paste the input exactly line by line in the Program input box and then run the active file. VoidLAB forwards the raw text to the runtime without rewriting it, so spaces, new lines, and multi-line cases stay intact.`,
5562
suggestions: [
56-
"Help me debug this output",
57-
"What languages are runnable right now?",
58-
"How do I use stdin in VoidLAB?",
63+
"Why is my program waiting for input?",
64+
"How is stdout different from stderr?",
65+
"How do I test multiple input cases?",
5966
],
6067
};
6168
}
6269

6370
if (
64-
normalized.includes("collab") ||
65-
normalized.includes("team") ||
66-
normalized.includes("share") ||
67-
normalized.includes("room")
71+
normalized.includes("error") ||
72+
normalized.includes("compile") ||
73+
normalized.includes("output") ||
74+
normalized.includes("wrong answer")
6875
) {
69-
const roomHint = context.roomId
70-
? ` You are currently connected to room ${context.roomId}.`
71-
: " Create a room, share the room code, and then use Push workspace to sync your current files into the shared session.";
76+
const statusHint = context.lastStatus ? ` The latest status was ${context.lastStatus}.` : "";
77+
const outputHint = context.lastOutput
78+
? ` The latest terminal output starts with: "${context.lastOutput.slice(0, 160)}".`
79+
: "";
7280

7381
return {
74-
reply: `VoidLAB collaboration rooms let teammates join the same workspace, exchange messages, and sync the latest file set into a shared room state.${roomHint}`,
82+
reply:
83+
`Start with the execution status, then check compile output, then stderr, then stdout. Compile output means the code did not build. Stderr means the program ran and failed. Stdout is the actual program result.${statusHint}${outputHint}${fileHint}`,
7584
suggestions: [
76-
"How do I create a room?",
77-
"How do I pull the latest shared workspace?",
78-
"How do I invite teammates?",
85+
"Explain my latest output",
86+
"How do I fix runtime errors?",
87+
"What if stdout is empty?",
7988
],
8089
};
8190
}
8291

83-
if (
84-
normalized.includes("manual") ||
85-
normalized.includes("shortcut") ||
86-
normalized.includes("how to")
87-
) {
92+
if (normalized.includes("command") || normalized.includes("terminal") || normalized.includes("folder")) {
8893
return {
8994
reply:
90-
"The fastest way to learn VoidLAB is to use the feature pages from the editor toolbar. Manual explains the product, GitHub shows publish commands, Collaboration manages shared rooms, and AI Guide helps with workflows and troubleshooting.",
95+
`VoidLAB workspace commands manage your virtual project tree. Use mkdir to create folders, touch to create files, ls or tree to inspect the workspace, open to jump to a file, and rm to remove entries. These commands operate on your workspace data directly, so they are fast and safe for browser use.${workspaceHint}`,
9196
suggestions: [
92-
"List the shortcuts",
93-
"What is the best workflow for a beginner?",
94-
"How do I deploy my project?",
97+
"List useful workspace commands",
98+
"How do I import a folder?",
99+
"Can I open a file from the terminal?",
95100
],
96101
};
97102
}
98103

99-
if (
100-
normalized.includes("html") ||
101-
normalized.includes("css") ||
102-
normalized.includes("preview")
103-
) {
104+
if (normalized.includes("profile") || normalized.includes("bio") || normalized.includes("social")) {
104105
return {
105106
reply:
106-
"For web-style files, VoidLAB can open a live preview in a new tab instead of sending the code to the compiler. If your workspace has HTML and CSS files together, the preview combines them so you can see the page instantly.",
107+
`Your profile page is where VoidLAB surfaces identity, bio, social links, and recent activities such as runs, saves, AI chats, and workspace changes. Keeping those details updated makes the product feel much more personal and portfolio-ready.`,
107108
suggestions: [
108-
"How do I preview HTML and CSS together?",
109-
"Can JavaScript be included in preview mode?",
110-
"Why does preview open in a new tab?",
109+
"What appears on the profile page?",
110+
"How are activities tracked?",
111+
"Can I update my social links later?",
111112
],
112113
};
113114
}
114115

115116
return {
116117
reply:
117-
"VoidLAB AI Guide is here to help with product usage, execution issues, workflow choices, collaboration, and deployment steps. Ask me about your current file, how to use a feature, or how to debug what just happened in the terminal.",
118+
`VoidLAB basic assistant is best for product usage, execution troubleshooting, workspace organization, and fast guidance while you code.${workspaceHint}${fileHint} Ask me about the current file, terminal output, or the next workflow step you want to take.`,
118119
suggestions: [
119-
"How do I use collaboration?",
120-
"Help me debug a failing run",
121-
"What can I do from the GitHub page?",
120+
"Help me debug my active file",
121+
"How do I use folders in VoidLAB?",
122+
"How do I run input-output programs correctly?",
122123
],
123124
};
124125
};
125126

127+
const getAssistantResponse = (context: AssistantContext | undefined, messages: ChatMessage[] | undefined) => {
128+
const lastUserMessage = messages?.filter((item) => item.role === "user").at(-1)?.content ?? "";
129+
return buildReply(lastUserMessage, context ?? {});
130+
};
131+
126132
export const chatWithAssistant = async (req: Request, res: Response) => {
127133
const { context, messages } = (req.body ?? {}) as {
128134
context?: AssistantContext;
129135
messages?: ChatMessage[];
130136
};
131137

132-
const lastUserMessage =
133-
messages?.filter((item) => item.role === "user").at(-1)?.content ?? "";
134-
135-
const response = buildReply(lastUserMessage, context ?? {});
138+
const response = getAssistantResponse(context, messages);
136139

137140
return res.status(200).json({
138141
ok: true,
139142
...response,
140143
});
141144
};
145+
146+
export const streamAssistantChat = async (req: Request, res: Response) => {
147+
const { context, messages } = (req.body ?? {}) as {
148+
context?: AssistantContext;
149+
messages?: ChatMessage[];
150+
};
151+
152+
const response = getAssistantResponse(context, messages);
153+
const chunks = response.reply.match(/.{1,32}(\s|$)|.{1,32}/g) ?? [response.reply];
154+
155+
res.setHeader("Cache-Control", "no-cache, no-transform");
156+
res.setHeader("Connection", "keep-alive");
157+
res.setHeader("Content-Type", "text/event-stream");
158+
res.flushHeaders?.();
159+
160+
emitEvent(res, "meta", { ok: true, mode: "basic-realtime" });
161+
162+
for (const chunk of chunks) {
163+
emitEvent(res, "chunk", { value: chunk });
164+
await delay(18);
165+
}
166+
167+
emitEvent(res, "done", {
168+
suggestions: response.suggestions,
169+
});
170+
res.end();
171+
};

apps/api/src/controllers/authControllers.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const sendWelcomeEmail = async ({
6868
};
6969

7070
export const createSession = async (req: Request, res: Response) => {
71-
const { email, name, phone, region } = req.body ?? {};
71+
const { bio, email, name, phone, region, socials } = req.body ?? {};
7272

7373
if (!email || !name || !phone || !region) {
7474
return res.status(400).json({
@@ -97,6 +97,21 @@ export const createSession = async (req: Request, res: Response) => {
9797
return res.status(200).json({
9898
ok: true,
9999
mail: emailStatus,
100-
profile: { email, name, phone, region },
100+
profile: {
101+
bio: typeof bio === "string" ? bio : "",
102+
email,
103+
name,
104+
phone,
105+
region,
106+
socials:
107+
socials && typeof socials === "object"
108+
? socials
109+
: {
110+
github: "",
111+
instagram: "",
112+
linkedin: "",
113+
x: "",
114+
},
115+
},
101116
});
102117
};

apps/api/src/controllers/collaborationController.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ type RoomFile = {
1111
id: string;
1212
languageId: string;
1313
name: string;
14+
path: string;
1415
};
1516

1617
type WorkspaceSnapshot = {
1718
activeFileId: string;
1819
files: RoomFile[];
20+
folders: string[];
1921
updatedAt: string;
2022
updatedBy: string;
2123
};
@@ -203,9 +205,10 @@ export const updateWorkspace = async (req: Request, res: Response) => {
203205

204206
if (!room) return;
205207

206-
const { activeFileId, files, participantId } = (req.body ?? {}) as {
208+
const { activeFileId, files, folders, participantId } = (req.body ?? {}) as {
207209
activeFileId?: string;
208210
files?: RoomFile[];
211+
folders?: string[];
209212
participantId?: string;
210213
};
211214

@@ -227,6 +230,7 @@ export const updateWorkspace = async (req: Request, res: Response) => {
227230
room.workspace = {
228231
activeFileId,
229232
files,
233+
folders: Array.isArray(folders) ? folders : [],
230234
updatedAt: new Date().toISOString(),
231235
updatedBy: author.name,
232236
};

apps/api/src/controllers/execute.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,84 @@
11
import axios from "axios";
22
import { Request, Response } from "express";
33

4-
const judge0ApiUrl =
5-
process.env.JUDGE0_API_URL ?? "https://ce.judge0.com";
4+
const judge0ApiUrl = process.env.JUDGE0_API_URL ?? "https://ce.judge0.com";
5+
6+
const encode = (value: string) => Buffer.from(value, "utf8").toString("base64");
7+
8+
const decode = (value?: string | null) => {
9+
if (!value) return "";
10+
11+
try {
12+
return Buffer.from(value, "base64").toString("utf8");
13+
} catch {
14+
return value;
15+
}
16+
};
617

718
export const executeCode = async (req: Request, res: Response) => {
819
const { code, languageId, stdin } = req.body ?? {};
920

10-
if (!code || !languageId) {
21+
if (typeof code !== "string" || !code.trim() || !languageId) {
1122
return res.status(400).json({
1223
error: "Language id and code are required.",
1324
});
1425
}
1526

1627
try {
1728
const response = await axios.post(
18-
`${judge0ApiUrl}/submissions/?base64_encoded=false&wait=true`,
29+
`${judge0ApiUrl}/submissions/?base64_encoded=true&wait=true`,
1930
{
2031
cpu_time_limit: 5,
21-
language_id: languageId,
32+
language_id: Number(languageId),
2233
memory_limit: 262144,
23-
source_code: code,
24-
stdin: typeof stdin === "string" ? stdin : "",
34+
source_code: encode(code),
35+
stdin: encode(typeof stdin === "string" ? stdin : ""),
2536
wall_time_limit: 12,
2637
},
2738
{
2839
headers: {
2940
"Content-Type": "application/json",
3041
"User-Agent": "VoidLAB/1.0",
3142
},
32-
timeout: 20000,
43+
timeout: 25000,
3344
},
3445
);
3546

36-
return res.status(200).json(response.data);
47+
const data = response.data ?? {};
48+
const stdout = decode(data.stdout);
49+
const stderr = decode(data.stderr);
50+
const compileOutput = decode(data.compile_output);
51+
const message = decode(data.message);
52+
const status = {
53+
description: data.status?.description ?? "Unknown",
54+
id: Number(data.status?.id ?? 0),
55+
successful: Number(data.status?.id ?? 0) === 3,
56+
};
57+
58+
return res.status(200).json({
59+
ok: true,
60+
execution: {
61+
compileOutput,
62+
memory: data.memory ?? null,
63+
message,
64+
output:
65+
[compileOutput, stderr, stdout, message]
66+
.filter((item) => typeof item === "string" && item.length > 0)
67+
.join("\n\n")
68+
.trim() || "Code executed with no output.",
69+
status,
70+
stderr,
71+
stdout,
72+
time: data.time ?? null,
73+
token: data.token ?? null,
74+
},
75+
});
3776
} catch (error) {
3877
const message =
3978
axios.isAxiosError(error) && error.response?.data
40-
? JSON.stringify(error.response.data)
79+
? typeof error.response.data === "string"
80+
? error.response.data
81+
: JSON.stringify(error.response.data)
4182
: "Compilation failed at the VoidLAB execution gateway.";
4283

4384
return res.status(500).json({ error: message });
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Router } from "express";
2-
import { chatWithAssistant } from "../controllers/assistantController";
2+
import { chatWithAssistant, streamAssistantChat } from "../controllers/assistantController";
33

44
const router = Router();
55

66
router.post("/chat", chatWithAssistant);
7+
router.post("/stream", streamAssistantChat);
78

89
export default router;

0 commit comments

Comments
 (0)