Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Use AI to explain code #2517

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 66 additions & 2 deletions frontend/components/CellInput.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { html, useState, useEffect, useLayoutEffect, useRef, useContext, useMemo } from "../imports/Preact.js"
import { html, useState, useEffect, useLayoutEffect, useRef, useContext, useMemo, useCallback } from "../imports/Preact.js"
import observablehq_for_myself from "../common/SetupCellEnvironment.js"
import _ from "../imports/lodash.js"

Expand All @@ -7,6 +7,8 @@ import { PlutoActionsContext } from "../common/PlutoContext.js"
import { get_selected_doc_from_state } from "./CellInput/LiveDocsFromCursor.js"
import { go_to_definition_plugin, GlobalDefinitionsFacet } from "./CellInput/go_to_definition_plugin.js"

import md from "https://esm.sh/markdown-js"

import {
EditorState,
EditorSelection,
Expand Down Expand Up @@ -64,6 +66,7 @@ import { commentKeymap } from "./CellInput/comment_mixed_parsers.js"
import { ScopeStateField } from "./CellInput/scopestate_statefield.js"
import { mod_d_command } from "./CellInput/mod_d_command.js"
import { open_bottom_right_panel } from "./BottomRightPanel.js"
import { open_pluto_popup } from "./Popup.js"

export const ENABLE_CM_MIXED_PARSER = window.localStorage.getItem("ENABLE_CM_MIXED_PARSER") === "true"

Expand Down Expand Up @@ -828,6 +831,51 @@ export const CellInput = ({
}
}, [cm_forced_focus])

const on_explain = useCallback(async () => {
const code = newcm_ref.current?.state?.doc?.toString?.()
const key = pluto_actions.get_openai_key()

if (code && key) {
const response = await fetch("https://api.openai.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${key}`,
},
body: JSON.stringify({
model: "text-davinci-003",
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you use gpt-3.5-turbo then that's cheaper and more accurate. The API call is slightly different because it's via the chat endpoint, but it should work for now and also for the people who have GPT-4 access. Also, maybe allow people to set the endpoint because things are moving fast in OpenAI-land?

prompt: `\`\`\`\n${code}\n\`\`\`\n\n${
code.length > 100 ? "Summarize the the intent of" : "Explain"
} this Julia code in a consise way. Use Markdown to highlight important concepts:\n1.`,
Comment on lines +847 to +849
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can now officially add prompt engineer to your resume 🚀

temperature: 0.3,
max_tokens: 256,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
}),
})

if (response.ok) {
const json = await response.json()
console.info("OpenAI API response", json)
const explanation = `1. ${json.choices[0].text}`

open_pluto_popup({
type: "info",
source_element: dom_node_ref.current,
body: html`${explanation
.split("\n")
.map((line) => html`<p dangerouslySetInnerHTML=${{ __html: md.makeHtml(line.replace(/^\d+\. /, "")) }}></p>`)}
<p><small>Powered by AI ✨</small></p>`,
})

return true
} else {
console.error("OpenAI API error", await response.text())
}
}
}, [])

return html`
<pluto-input ref=${dom_node_ref} class="CodeMirror" translate=${false}>
<${InputContextMenu}
Expand All @@ -840,12 +888,24 @@ export const CellInput = ({
show_logs=${show_logs}
set_show_logs=${set_show_logs}
set_cell_disabled=${set_cell_disabled}
on_explain=${on_explain}
/>
</pluto-input>
`
}

const InputContextMenu = ({ on_delete, cell_id, run_cell, skip_as_script, running_disabled, any_logs, show_logs, set_show_logs, set_cell_disabled }) => {
const InputContextMenu = ({
on_delete,
cell_id,
run_cell,
skip_as_script,
running_disabled,
any_logs,
show_logs,
set_show_logs,
set_cell_disabled,
on_explain,
}) => {
const timeout = useRef(null)
let pluto_actions = useContext(PlutoActionsContext)
const [open, setOpen] = useState(false)
Expand Down Expand Up @@ -922,6 +982,10 @@ const InputContextMenu = ({ on_delete, cell_id, run_cell, skip_as_script, runnin
${skip_as_script ? html`<span class="skip_as_script ctx_icon" />` : html`<span class="run_as_script ctx_icon" />`}
${skip_as_script ? html`<b>Enable in file</b>` : html`Disable in file`}
</li>
<li onClick=${on_explain} title="Ask OpenAI to explain this code">
<span class="ai_explain ctx_icon" />
${html`Explain code`}
</li>
</ul>`
: html``}
</button>`
Expand Down
1 change: 1 addition & 0 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ export class Editor extends Component {
const { message } = await this.client.send("nbpkg_available_versions", { package_name: package_name }, { notebook_id: notebook_id })
return message.versions
},
get_openai_key: () => this.client.session_options?.server?._experimental_openai_key,
}
this.actions = { ...this.real_actions }

Expand Down
3 changes: 3 additions & 0 deletions frontend/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,9 @@ pluto-input > button.input_context_menu ul {
.ctx_icon.skip_as_script {
background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/[email protected]/src/svg/document-text-outline.svg");
}
.ctx_icon.ai_explain {
background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/[email protected]/src/svg/flower-outline.svg");
}

.ctx_icon.copy_output {
background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/[email protected]/src/svg/copy-outline.svg");
Expand Down
4 changes: 4 additions & 0 deletions src/Configuration.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const PORT_HINT_DEFAULT = 1234
const LAUNCH_BROWSER_DEFAULT = true
const DISMISS_UPDATE_NOTIFICATION_DEFAULT = false
const SHOW_FILE_SYSTEM_DEFAULT = true
const _EXPERIMENTAL_OPENAI_KEY_DEFAULT = nothing
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe the secret should be inserted in the browser and be kept in the browser storage? That saves the hassle with having to make the secret key available inside the Julia process and might therefore be more secure?

const ENABLE_PACKAGE_AUTHOR_FEATURES_DEFAULT = true
const DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT = false
const AUTO_RELOAD_FROM_FILE_DEFAULT = false
Expand Down Expand Up @@ -94,6 +95,7 @@ The HTTP server options. See [`SecurityOptions`](@ref) for additional settings.
launch_browser::Bool = LAUNCH_BROWSER_DEFAULT
dismiss_update_notification::Bool = DISMISS_UPDATE_NOTIFICATION_DEFAULT
show_file_system::Bool = SHOW_FILE_SYSTEM_DEFAULT
_experimental_openai_key::Union{String,Nothing} = _EXPERIMENTAL_OPENAI_KEY_DEFAULT
notebook_path_suggestion::String = notebook_path_suggestion()
disable_writing_notebook_files::Bool = DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT
auto_reload_from_file::Bool = AUTO_RELOAD_FROM_FILE_DEFAULT
Expand Down Expand Up @@ -275,6 +277,7 @@ function from_flat_kwargs(;
launch_browser::Bool = LAUNCH_BROWSER_DEFAULT,
dismiss_update_notification::Bool = DISMISS_UPDATE_NOTIFICATION_DEFAULT,
show_file_system::Bool = SHOW_FILE_SYSTEM_DEFAULT,
_experimental_openai_key::Union{String,Nothing} = _EXPERIMENTAL_OPENAI_KEY_DEFAULT,
notebook_path_suggestion::String = notebook_path_suggestion(),
disable_writing_notebook_files::Bool = DISABLE_WRITING_NOTEBOOK_FILES_DEFAULT,
auto_reload_from_file::Bool = AUTO_RELOAD_FROM_FILE_DEFAULT,
Expand Down Expand Up @@ -321,6 +324,7 @@ function from_flat_kwargs(;
launch_browser,
dismiss_update_notification,
show_file_system,
_experimental_openai_key,
notebook_path_suggestion,
disable_writing_notebook_files,
auto_reload_from_file,
Expand Down