Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions theme/common.less
Original file line number Diff line number Diff line change
Expand Up @@ -2155,3 +2155,39 @@ select.ui.dropdown {
background-color: #F5BC5F;
}
}


//////////////////////////////////////////////////
// AI ASSISTANT //
//////////////////////////////////////////////////

.assistant-container {
position: fixed;
bottom: 7rem;
right: 2rem;

height: 18rem;
width: 32rem;
padding: 1rem;
background: #fff;
box-shadow: 0 0 0 1px rgba(34,36,38,.15);

border-radius: 0.2rem;
z-index: 1000000;
}

.assistant-input {
display: flex;

button {
background: none !important;
}

input {
flex: 1;
outline: none !important;
border: none !important;
padding-left: 1rem;
box-shadow: 0 0 0 1px rgba(34,36,38,.15);
}
}
5 changes: 5 additions & 0 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import Util = pxt.Util;
import { HintManager } from "./hinttooltip";
import { CodeCardView } from "./codecard";
import { mergeProjectCode } from "./mergeProjects";
import { Assistant } from "./components/Assistant";

pxsim.util.injectPolyphils();

Expand Down Expand Up @@ -4497,6 +4498,9 @@ export class ProjectView
&& !(isBlocks
|| (pkg.mainPkg && pkg.mainPkg.config && (pkg.mainPkg.config.preferredEditor == pxt.BLOCKS_PROJECT_NAME)));
const hasIdentity = pxt.auth.hasIdentity();

const currentSrc = pkg?.mainEditorPkg()?.files[pxt.MAIN_TS]?.content;

return (
<div id='root' className={rootClasses}>
{greenScreen ? <greenscreen.WebCam close={this.toggleGreenScreen} /> : undefined}
Expand Down Expand Up @@ -4561,6 +4565,7 @@ export class ProjectView
{hideMenuBar ? <div id="editorlogo"><a className="poweredbylogo"></a></div> : undefined}
{lightbox ? <sui.Dimmer isOpen={true} active={lightbox} portalClassName={'tutorial'} className={'ui modal'}
shouldFocusAfterRender={false} closable={true} onClose={this.hideLightbox} /> : undefined}
{!inHome && <Assistant parent={this} userCode={currentSrc} />}
</div>
);
}
Expand Down
104 changes: 104 additions & 0 deletions webapp/src/components/Assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as React from "react";

import { Button } from "../sui";
import { MarkedContent } from "../marked";

interface AssistantProps {
parent: pxt.editor.IProjectView;
userCode?: string;
}

async function getCompletions(prompt: string, callback: (text: string) => void) {
let resp = await pxt.Util.requestAsync({
url: `https://api.openai.com/v1/engines/davinci-codex-msft/completions`,
method: "POST",
data: {
"prompt": prompt,
"max_tokens": 64,
"temperature": 0,
"top_p": 1,
"n": 1,
"stream": false,
"stop": "//",
},
headers: {"Authorization": "// ADD USER TOKEN"}
Copy link

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Authorization header currently uses a placeholder token. Replace it with a secure mechanism (e.g., via environment variables) before deploying to production.

Suggested change
headers: {"Authorization": "// ADD USER TOKEN"}
headers: {"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`}

Copilot uses AI. Check for mistakes.
})

callback(resp.json.choices?.[0]?.text);
}

function addHeader(prompt: string) {
const header = "// If asked something conversational, use console.log to answer\n\n";
return header + `\n\n` + prompt;
}

function addSamples(prompt: string) {
const samples = `// Create a sprite character
let mySprite = sprites.create(img\`.\`, SpriteKind.Player)
// Move the sprite with the d-pad buttons
controller.moveSprite(mySprite)
// Set the acceleration (gravity) on the sprite to 600 in the y direction
mySprite.ay = 600

// Run some code when the B button is pressed
controller.B.onEvent(ControllerButtonEvent.Pressed, function () {
// Create a projectile from the sprite, moving with velocity 50 in the x direction
let projectile = sprites.createProjectileFromSprite(img\`.\`, mySprite, 50, 0)
})
`
return prompt + `\n` + samples;
}

function addUserCode(prompt: string, userCode: string) {
// Strip image literals
userCode = userCode.replace(/img\s*`[\s\da-f.#tngrpoyw]*`\s*/g, "img` `");

return prompt + `\n` + userCode;
}

function getUserVariableDeclarations(userCode: string) {
let declarations: { name: string, declaration: string }[] = [];
userCode.replace(/let ([\S]+)\s*(?::\s*[\S]+)? = null/gi, (m0, m1) => {
Copy link

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The regex used to extract variable declarations only targets variables initialized to null. Consider a more robust approach if additional variable initialization patterns need to be supported.

Suggested change
userCode.replace(/let ([\S]+)\s*(?::\s*[\S]+)? = null/gi, (m0, m1) => {
userCode.replace(/let ([\S]+)\s*(?::\s*[\S]+)? = [^;\n]+/gi, (m0, m1) => {

Copilot uses AI. Check for mistakes.
declarations.push({
name: m1,
declaration: m0
})
return m0
});

return declarations;
}

export function Assistant(props: AssistantProps) {
const { parent, userCode } = props;
const [ question, setQuestion ] = React.useState("");
const [ markdown, setMarkdown ] = React.useState(`\`\`\`blocks\nlet x = 2\n\`\`\``);
const declarations = userCode && getUserVariableDeclarations(userCode);

const renderAnswer = (completion: string) => {
console.log('resp', completion);
let usedVariables = declarations.filter(el => completion.indexOf(el.name) >= 0).map(el => el.declaration);
setMarkdown(`\`\`\`blocks\n${usedVariables.join(`\n`)}\n${completion}\n\`\`\``)
}

const getAnswer = () => {
let prompt = "";

prompt = addHeader(prompt);
// prompt = addSamples(prompt);
prompt = addUserCode(prompt, userCode);
prompt += `\n\n// ${question}\n`;
console.log(prompt)
getCompletions(prompt, renderAnswer);
}

return <div className="assistant-container">
<div className="assistant-input">
<input value={question} onChange={e => setQuestion(e.target.value)} placeholder="How do I..." />
<Button icon="search" className="attached right" onClick={getAnswer} />
</div>
<div className="assistant-response">
<MarkedContent parent={parent} markdown={markdown} unboxSnippets={true} />
</div>
</div>
}