diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 3834200..de5e524 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -81,6 +81,7 @@ const config = { position: 'left', label: 'Docs', }, + {to: '/playground', label: 'Playground', position: 'left'}, {to: '/api', label: 'API', position: 'left'}, // FIXME(meng): enable blog. // {to: '/blog', label: 'Blog', position: 'left'}, @@ -97,11 +98,19 @@ const config = { style: 'dark', links: [ { - title: 'Docs', + title: 'Links', items: [ { - label: 'Self Hosting', - to: '/docs/self-hosting', + label: 'Docs', + to: '/docs/getting-started', + }, + { + label: 'Playground', + to: '/playground', + }, + { + label: 'API', + to: '/api', }, ], }, diff --git a/website/package.json b/website/package.json index e11c6cd..4d7b385 100644 --- a/website/package.json +++ b/website/package.json @@ -17,7 +17,9 @@ "@docusaurus/core": "2.4.1", "@docusaurus/preset-classic": "2.4.1", "@mdx-js/react": "^1.6.22", + "@monaco-editor/react": "^4.5.1", "autoprefixer": "^10.4.14", + "axios": "^1.4.0", "clsx": "^1.2.1", "docusaurus-preset-openapi": "^0.6.4", "postcss": "^8.4.24", diff --git a/website/src/components/Monaco/index.js b/website/src/components/Monaco/index.js new file mode 100644 index 0000000..fb72168 --- /dev/null +++ b/website/src/components/Monaco/index.js @@ -0,0 +1,185 @@ +import axios from "axios" + +import React, { useRef, useEffect } from "react" +import Editor, { useMonaco } from "@monaco-editor/react" + +let TabbyServerURL = "https://app.tabbyml.com/api/workspace/tabbyml/tabby"; + +export default function MonacoEditor(props) { + function beforeMount(monaco) { + // Setup theming + const LIGHT_BACKGROUND = getColor( + "--ifm-background-color" + ); + + monaco.editor.defineTheme("tabby-light", { + base: "vs", + inherit: true, + rules: [], + colors: { + "editor.background": LIGHT_BACKGROUND, + } + }); + + monaco.languages.registerInlineCompletionsProvider( + { pattern: "**" }, + new CompletionProvider(monaco) + ) + + monaco.editor.registerCommand( + "acceptTabbyCompletion", + (accessor, id, index) => { + logAction(id, index, "select") + } + ) + } + + return ( + + ) +} + +class CompletionProvider { + constructor(monaco) { + this.monaco = monaco + this.latestTimestamp = 0 + } + + async provideInlineCompletions(document, position, context, token) { + const prompt = this.getPrompt(document, position) + const emptyResponse = Promise.resolve({ items: [] }) + + if (this.isNil(prompt)) { + console.debug("Prompt is empty, skipping") + return emptyResponse + } + + const currentTimestamp = Date.now() + this.latestTimestamp = currentTimestamp + + await this.sleep(500) + if (this.pendingRequest) await this.pendingRequest + if (currentTimestamp < this.latestTimestamp) { + return emptyResponse + } + + let response + try { + response = await this.callTabbyApi(currentTimestamp, prompt) + } catch (err) { + console.error("error", err) + return emptyResponse + } + const hasSuffixParen = this.hasSuffixParen(document, position) + const replaceRange = hasSuffixParen + ? new this.monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + 1 + ) + : new this.monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ) + const items = this.toInlineCompletions(response.data, replaceRange) + return Promise.resolve({ data: response.data, items }) + } + + handleItemDidShow(completions, item) { + logAction(completions.data.id, item.choice.index, "view") + } + + freeInlineCompletions() {} + + getPrompt(document, position) { + const firstLine = Math.max(position.lineNumber - 120, 0) + + const range = new this.monaco.Range( + firstLine, + 0, + position.lineNumber, + position.column + ) + return document.getValueInRange(range) + } + + isNil(value) { + return value === undefined || value === null || value.length === 0 + } + + sleep(milliseconds) { + return new Promise((r) => setTimeout(r, milliseconds)) + } + + async callTabbyApi(timestamp, prompt) { + const request = (this.pendingRequest = axios.post( + `${TabbyServerURL}/v1/completions`, + { + language: "python", + prompt, + } + )) + const response = await request + this.pendingRequest = null + return response + } + + toInlineCompletions(value, range) { + return ( + value.choices.map((choice) => ({ + range, + insertText: choice.text, + choice, + command: { + id: "acceptTabbyCompletion", + arguments: [value.id, choice.index], + }, + })) || [] + ) + } + + hasSuffixParen(document, position) { + const suffix = document.getValueInRange( + new this.monaco.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + 1 + ) + ) + return ")]}".indexOf(suffix) > -1 + } +} + +function logAction(completion_id, choice_index, type) { + axios.post(`${TabbyServerURL}/v1/events`, { + type, + completion_id, + choice_index, + }) +} + +function getColor(property) { + const styles = getComputedStyle(document.documentElement); + // Weird chrome bug, returns " #ffffff " instead of "#ffffff", see: https://github.com/cloud-annotations/docusaurus-openapi/issues/144 + const color = styles.getPropertyValue(property).trim(); + if (color.length === 4) { + // change hex short codes like "#fff" to "#ffffff" + // to fix: https://github.com/cloud-annotations/docusaurus-openapi/issues/183 + let res = "#"; // prepend # + for (const c of color.substring(1)) { + res += c + c; // duplicate each char + } + return res; + } + return color; +} + diff --git a/website/src/css/custom.css b/website/src/css/custom.css index f00f930..41ec67e 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -18,17 +18,14 @@ --ifm-color-primary-lighter: #443f45; --ifm-color-primary-lightest: #4d484e; - --ifm-background-color: #e9e1d4; - --ifm-background-surface-color: var(--ifm-background-color); + --ifm-background-color: #FFF8EC; + --ifm-background-surface-color: #E9E1D4; - --ifm-code-background: #FFF8EC; - --openapi-monaco-background-color-light: #FFF8EC !important; + --ifm-code-background: #FFFEFD; + --openapi-monaco-background-color-light: var(--ifm-code-background) !important; --openapi-card-background-color: var(--ifm-code-background) !important; } -.monaco-editor{ - background-color: var(--openapi-monaco-background-color-light) !important; -} .markdown a { color: var(--ifm-color-success-darkest); @@ -56,3 +53,7 @@ height: 10px; font-weight: 600; } + +.Playground .monaco-editor { + padding-top: 15px; +} diff --git a/website/src/pages/playground.js b/website/src/pages/playground.js new file mode 100644 index 0000000..85c92aa --- /dev/null +++ b/website/src/pages/playground.js @@ -0,0 +1,33 @@ +import React from 'react'; +import Layout from '@theme/Layout'; +import Monaco from "@site/src/components/Monaco"; + +export default function Home() { + return ( + +
+
+ +
+
+
+ ); +} + +const DEFAULT_CODE = `import datetime + +def parse_expenses(expenses_string): + """Parse the list of expenses and return the list of triples (date, value, currency). + Ignore lines starting with #. + Parse the date using datetime. + Example expenses_string: + 2016-01-02 -34.01 USD + 2016-01-03 2.59 DKK + 2016-01-03 -2.72 EUR + """` diff --git a/website/yarn.lock b/website/yarn.lock index 27f2e86..cbf6595 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1804,7 +1804,7 @@ dependencies: state-local "^1.0.6" -"@monaco-editor/react@^4.3.1": +"@monaco-editor/react@^4.3.1", "@monaco-editor/react@^4.5.1": version "4.5.1" resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.5.1.tgz#fbc76c692aee9a33b9ab24ae0c5f219b8f002fdb" integrity sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ== @@ -2681,6 +2681,15 @@ axios@^0.26.1: dependencies: follow-redirects "^1.14.8" +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-loader@^8.2.5: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -4257,7 +4266,7 @@ flux@^4.0.1: fbemitter "^3.0.0" fbjs "^3.0.1" -follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.14.8: +follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.14.8, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -6781,6 +6790,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"