mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 16:51:55 +00:00
cody: add the client side app to review CLI generated code completions (#53140)
- Adds the client-side app to review CLI-generated code completions. - This skeleton is generated via npx create-remix --template https://github.com/remix-run/remix/tree/templates_v2_dev/templates/express - Remix is used because: - It's easy to bootstrap the application with templates. - It makes it easy to process completions on the server and visualize them on the client. - The DX is great out of the box (hot-reload, faster eslint-powered builds, CSS modules) - Aligned with our current stack (Typescript, React, React-router, CSS modules) <img width="1200" alt="Screenshot 2023-06-07 at 17 16 12" src="https://github.com/sourcegraph/sourcegraph/assets/3846380/422e77f7-30ff-46f9-8877-00026077678c"> ## Test plan From the root of the repo: 1. `SOURCEGRAPH_ACCESS_TOKEN=XXX pnpm --filter cody-ai run generate:completions` 2. `pnpm --filter @sourcegraph/completions-review-tool run dev` 3. open http://localhost:3000/
This commit is contained in:
parent
54ec2cb29e
commit
dc8fc49713
@ -33,6 +33,7 @@ client/vscode/node_modules
|
||||
dev/release/node_modules
|
||||
client/plugin-backstage/node_modules
|
||||
client/app-shell/node_modules
|
||||
client/completions-review-tool/node_modules
|
||||
|
||||
cmd/symbols/squirrel/test_repos/starlark
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ code-intel-extensions.json
|
||||
pnpm-lock.yaml
|
||||
node_modules/
|
||||
client/web-sveltekit/.svelte-kit
|
||||
client/completions-review-tool/data/
|
||||
*.module.scss.d.ts
|
||||
.vscode-test
|
||||
|
||||
|
||||
0
client/completions-review-tool/.eslintignore
Normal file
0
client/completions-review-tool/.eslintignore
Normal file
27
client/completions-review-tool/.eslintrc.js
Normal file
27
client/completions-review-tool/.eslintrc.js
Normal file
@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
|
||||
const baseConfig = require('../../.eslintrc.js')
|
||||
|
||||
module.exports = {
|
||||
extends: '../../.eslintrc.js',
|
||||
parserOptions: {
|
||||
...baseConfig.parserOptions,
|
||||
project: [__dirname + '/tsconfig.json'],
|
||||
},
|
||||
overrides: baseConfig.overrides,
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'no-restricted-imports': 'off',
|
||||
'react/forbid-dom-props': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
'no-console': 'off',
|
||||
'no-duplicate-imports': 'off',
|
||||
'arrow-body-style': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'ban/ban': 'off',
|
||||
'no-sync': 'off',
|
||||
},
|
||||
}
|
||||
7
client/completions-review-tool/.gitignore
vendored
Normal file
7
client/completions-review-tool/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
||||
48
client/completions-review-tool/BUILD.bazel
generated
Normal file
48
client/completions-review-tool/BUILD.bazel
generated
Normal file
@ -0,0 +1,48 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("//dev:defs.bzl", "ts_project")
|
||||
load("//dev:eslint.bzl", "eslint_config_and_lint_root")
|
||||
|
||||
# gazelle:js_ignore_imports **/*.css
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
eslint_config_and_lint_root()
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = ["//client:__subpackages__"],
|
||||
deps = [
|
||||
"//:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "completions-review-tool",
|
||||
srcs = [
|
||||
"globals.d.ts",
|
||||
"src/entry.client.tsx",
|
||||
"src/entry.server.tsx",
|
||||
"src/models/completions.server.ts",
|
||||
"src/root.tsx",
|
||||
"src/routes/_index.tsx",
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
":node_modules/@remix-run/css-bundle",
|
||||
":node_modules/@remix-run/node",
|
||||
":node_modules/@remix-run/react",
|
||||
":node_modules/@types/react-table",
|
||||
":node_modules/isbot",
|
||||
":node_modules/react",
|
||||
":node_modules/react-dom",
|
||||
":node_modules/react-table",
|
||||
"//:node_modules/@remix-run/server-runtime", #keep
|
||||
"//:node_modules/@types/marked",
|
||||
"//:node_modules/@types/node",
|
||||
"//:node_modules/@types/react",
|
||||
"//:node_modules/@types/react-dom",
|
||||
"//:node_modules/marked",
|
||||
],
|
||||
)
|
||||
27
client/completions-review-tool/README.md
Normal file
27
client/completions-review-tool/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Welcome to the Completions Review tool!
|
||||
|
||||
- Generated with [Remix](https://remix.run/docs)
|
||||
|
||||
## Development
|
||||
|
||||
Start the Remix development asset server and the Express server by running:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This starts your app in development mode, which will purge the server require cache when Remix rebuilds assets so you don't need a process manager restarting the express server.
|
||||
|
||||
## Deployment
|
||||
|
||||
First, build your app for production:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then run the app in production mode:
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
@ -0,0 +1,25 @@
|
||||
[
|
||||
{
|
||||
"completions": [
|
||||
"s"
|
||||
],
|
||||
"timestamp": "1686127320776",
|
||||
"code": "import signale from 'signale'\n\nfunction logMessage(message: string) {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"throw new Error('Not yet implemented');}",
|
||||
"throw new Error(\"Not implemented.\");"
|
||||
],
|
||||
"timestamp": "1686127320776",
|
||||
"code": "class TextDocument implements vscode.TextDocument {\n private text: string\n\n constructor(public uri: vscode.Uri, text: string) {\n this.text = text.replace(/\r\n/gm, '\n') // normalize end of line\n }\n\n private get lines(): string[] {\n return this.text.split('\n')\n }\n\n lineAt(position: number | vscode.Position): vscode.TextLine {\n ️🔥\n }\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"const date = new Date()",
|
||||
"const fileName = path.join(__dirname, 'date.txt')"
|
||||
],
|
||||
"timestamp": "1686127320776",
|
||||
"code": "import path from 'path'\n\nfunction writeDateToDisk() {\n ️🔥\n}"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"completions": [
|
||||
"signale.info(message)",
|
||||
"signale.info(message)"
|
||||
],
|
||||
"timestamp": "1686127445971",
|
||||
"code": "import signale from 'signale'\n\nfunction logMessage(message: string) {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"const date = new Date()",
|
||||
"const dateString = new Date().toISOString()",
|
||||
"const filename = 'file.txt'"
|
||||
],
|
||||
"timestamp": "1686127445971",
|
||||
"code": "import path from 'path'\n\nfunction writeDateToDisk() {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"throw new Error('Not implemented.');"
|
||||
],
|
||||
"timestamp": "1686127445971",
|
||||
"code": "class TextDocument implements vscode.TextDocument {\n private text: string\n\n constructor(public uri: vscode.Uri, text: string) {\n this.text = text.replace(/\r\n/gm, '\n') // normalize end of line\n }\n\n private get lines(): string[] {\n return this.text.split('\n')\n }\n\n lineAt(position: number | vscode.Position): vscode.TextLine {\n ️🔥\n }\n}"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"completions": [
|
||||
"signale.info(message)",
|
||||
"signale.log(message);"
|
||||
],
|
||||
"timestamp": "1686127449821",
|
||||
"code": "import signale from 'signale'\n\nfunction logMessage(message: string) {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"const now = n",
|
||||
"const dateStr = new Date().toISOString()",
|
||||
"const now = new Date()"
|
||||
],
|
||||
"timestamp": "1686127449821",
|
||||
"code": "import path from 'path'\n\nfunction writeDateToDisk() {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"throw new Error('Not implemented')",
|
||||
"throw new Error('unimplemented')"
|
||||
],
|
||||
"timestamp": "1686127449821",
|
||||
"code": "class TextDocument implements vscode.TextDocument {\n private text: string\n\n constructor(public uri: vscode.Uri, text: string) {\n this.text = text.replace(/\r\n/gm, '\n') // normalize end of line\n }\n\n private get lines(): string[] {\n return this.text.split('\n')\n }\n\n lineAt(position: number | vscode.Position): vscode.TextLine {\n ️🔥\n }\n}"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"completions": [
|
||||
"const d = new Date()",
|
||||
"const date =",
|
||||
"const p = path.j"
|
||||
],
|
||||
"timestamp": "1686127552793",
|
||||
"code": "import path from 'path'\n\nfunction writeDateToDisk() {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"signale.success(message);"
|
||||
],
|
||||
"timestamp": "1686127552793",
|
||||
"code": "import signale from 'signale'\n\nfunction logMessage(message: string) {\n ️🔥\n}"
|
||||
},
|
||||
{
|
||||
"completions": [
|
||||
"throw new Error('Not implemented')",
|
||||
"// TODO"
|
||||
],
|
||||
"timestamp": "1686127552793",
|
||||
"code": "class TextDocument implements vscode.TextDocument {\n private text: string\n\n constructor(public uri: vscode.Uri, text: string) {\n this.text = text.replace(/\r\n/gm, '\n') // normalize end of line\n }\n\n private get lines(): string[] {\n return this.text.split('\n')\n }\n\n lineAt(position: number | vscode.Position): vscode.TextLine {\n ️🔥\n }\n}"
|
||||
}
|
||||
]
|
||||
9
client/completions-review-tool/globals.d.ts
vendored
Normal file
9
client/completions-review-tool/globals.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const cssModule: string
|
||||
export default cssModule
|
||||
}
|
||||
36
client/completions-review-tool/package.json
Normal file
36
client/completions-review-tool/package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@sourcegraph/completions-review-tool",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "npm-run-all build --parallel \"dev:*\"",
|
||||
"dev:node": "cross-env NODE_ENV=development nodemon --require dotenv/config ./server.js --watch ./server.js",
|
||||
"dev:remix": "remix watch",
|
||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/css-bundle": "^1.17.0",
|
||||
"@remix-run/express": "^1.17.0",
|
||||
"@remix-run/node": "^1.17.0",
|
||||
"@remix-run/react": "^1.17.0",
|
||||
"@remix-run/server-runtime": "^1.17.0",
|
||||
"isbot": "^3.6.8",
|
||||
"morgan": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-table": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.17.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"dotenv": "^16.0.3",
|
||||
"npm-run-all": "^4.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
17
client/completions-review-tool/remix.config.js
Normal file
17
client/completions-review-tool/remix.config.js
Normal file
@ -0,0 +1,17 @@
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
ignoredRouteFiles: ['**/.*'],
|
||||
appDirectory: 'src',
|
||||
assetsBuildDirectory: 'public/build',
|
||||
serverBuildPath: 'build/index.js',
|
||||
publicPath: '/build/',
|
||||
serverDependenciesToBundle: 'all',
|
||||
serverModuleFormat: 'cjs',
|
||||
future: {
|
||||
v2_errorBoundary: true,
|
||||
v2_headers: true,
|
||||
v2_meta: true,
|
||||
v2_normalizeFormMethod: true,
|
||||
v2_routeConvention: true,
|
||||
},
|
||||
}
|
||||
2
client/completions-review-tool/remix.env.d.ts
vendored
Normal file
2
client/completions-review-tool/remix.env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node" />
|
||||
64
client/completions-review-tool/server.js
Normal file
64
client/completions-review-tool/server.js
Normal file
@ -0,0 +1,64 @@
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
const path = require('path')
|
||||
|
||||
const { createRequestHandler } = require('@remix-run/express')
|
||||
const { installGlobals } = require('@remix-run/node')
|
||||
const compression = require('compression')
|
||||
const express = require('express')
|
||||
const morgan = require('morgan')
|
||||
|
||||
installGlobals()
|
||||
|
||||
const BUILD_DIR = path.join(process.cwd(), 'build')
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(compression())
|
||||
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by')
|
||||
|
||||
// Remix fingerprints its assets so we can cache forever.
|
||||
app.use('/build', express.static('public/build', { immutable: true, maxAge: '1y' }))
|
||||
|
||||
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
||||
// more aggressive with this caching.
|
||||
app.use(express.static('public', { maxAge: '1h' }))
|
||||
|
||||
app.use(morgan('tiny'))
|
||||
|
||||
app.all(
|
||||
'*',
|
||||
process.env.NODE_ENV === 'development'
|
||||
? (req, res, next) => {
|
||||
purgeRequireCache()
|
||||
|
||||
return createRequestHandler({
|
||||
build: require(BUILD_DIR),
|
||||
mode: process.env.NODE_ENV,
|
||||
})(req, res, next)
|
||||
}
|
||||
: createRequestHandler({
|
||||
build: require(BUILD_DIR),
|
||||
mode: process.env.NODE_ENV,
|
||||
})
|
||||
)
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
app.listen(port, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Express server listening on port ${port}`)
|
||||
})
|
||||
|
||||
function purgeRequireCache() {
|
||||
// purge require cache on requests for "server side HMR" this won't let
|
||||
// you have in-memory objects between requests in development,
|
||||
// alternatively you can set up nodemon/pm2-dev to restart the server on
|
||||
// file changes, but then you'll have to reconnect to databases/etc on each
|
||||
// change. We prefer the DX of this, so we've included it for you by default
|
||||
for (const key in require.cache) {
|
||||
if (key.startsWith(BUILD_DIR)) {
|
||||
delete require.cache[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
.table-wrapper {
|
||||
overflow: scroll;
|
||||
flex-grow: 1;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.table {
|
||||
/* */
|
||||
}
|
||||
|
||||
.table thead tr > th {
|
||||
background: #ccc;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.table thead tr > :first-child {
|
||||
z-index: 3;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.table tbody tr > :first-child {
|
||||
background: #ddd;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
}
|
||||
22
client/completions-review-tool/src/entry.client.tsx
Normal file
22
client/completions-review-tool/src/entry.client.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* By default, Remix will handle hydrating your app on the client for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.client
|
||||
*/
|
||||
|
||||
import { startTransition, StrictMode } from 'react'
|
||||
|
||||
import { RemixBrowser } from '@remix-run/react'
|
||||
import { hydrate } from 'react-dom'
|
||||
|
||||
// Throws on hydration 🫠
|
||||
// import { hydrateRoot } from 'react-dom/client'
|
||||
|
||||
startTransition(() => {
|
||||
hydrate(
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>,
|
||||
document
|
||||
)
|
||||
})
|
||||
117
client/completions-review-tool/src/entry.server.tsx
Normal file
117
client/completions-review-tool/src/entry.server.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* By default, Remix will handle generating the HTTP Response for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.server
|
||||
*/
|
||||
|
||||
import { AppLoadContext, EntryContext, Response } from '@remix-run/node'
|
||||
import { RemixServer } from '@remix-run/react'
|
||||
import isbot from 'isbot'
|
||||
import { renderToPipeableStream } from 'react-dom/server'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { PassThrough } = require('node:stream')
|
||||
|
||||
const ABORT_DELAY = 5_000
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
loadContext: AppLoadContext
|
||||
) {
|
||||
return isbot(request.headers.get('user-agent'))
|
||||
? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
|
||||
: handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext)
|
||||
}
|
||||
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
|
||||
{
|
||||
onAllReady() {
|
||||
shellRendered = true
|
||||
const body = new PassThrough()
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html')
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
)
|
||||
|
||||
pipe(body)
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error)
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
setTimeout(abort, ABORT_DELAY)
|
||||
})
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
|
||||
{
|
||||
onShellReady() {
|
||||
shellRendered = true
|
||||
const body = new PassThrough()
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html')
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
)
|
||||
|
||||
pipe(body)
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error)
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
setTimeout(abort, ABORT_DELAY)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// Read completions from the 'data' directory
|
||||
const dataDir = path.join(__dirname, '../', 'data')
|
||||
const jsonFiles = fs.readdirSync(dataDir).filter(file => file.endsWith('.json'))
|
||||
|
||||
type CompletionData = {
|
||||
code: string
|
||||
completions: string[]
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
const data: CompletionData[] = []
|
||||
|
||||
jsonFiles.forEach(file => {
|
||||
const filePath = path.join(dataDir, file)
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const jsonData = JSON.parse(fileContent)
|
||||
data.push(...jsonData)
|
||||
})
|
||||
|
||||
type CodeToCompletions = Record<string, Omit<CompletionData, 'code'>[]>
|
||||
|
||||
const codeToCompletions: CodeToCompletions = {}
|
||||
|
||||
// Group data by `code`.
|
||||
data.forEach(({ code, completions, timestamp }) => {
|
||||
if (!codeToCompletions[code]) {
|
||||
codeToCompletions[code] = []
|
||||
}
|
||||
|
||||
codeToCompletions[code].push({ completions, timestamp })
|
||||
})
|
||||
|
||||
export function getCompletions(): Promise<CodeToCompletions> {
|
||||
return Promise.resolve(codeToCompletions)
|
||||
}
|
||||
29
client/completions-review-tool/src/root.tsx
Normal file
29
client/completions-review-tool/src/root.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { cssBundleHref } from '@remix-run/css-bundle'
|
||||
import type { LinksFunction } from '@remix-run/node'
|
||||
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'
|
||||
|
||||
import styles from './styles/global.css'
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: 'stylesheet', href: styles },
|
||||
...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []),
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
120
client/completions-review-tool/src/routes/_index.tsx
Normal file
120
client/completions-review-tool/src/routes/_index.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { json } from '@remix-run/node'
|
||||
import type { V2_MetaFunction } from '@remix-run/node'
|
||||
import { useLoaderData } from '@remix-run/react'
|
||||
import { marked } from 'marked'
|
||||
import { useTable } from 'react-table'
|
||||
|
||||
import { getCompletions } from '../models/completions.server'
|
||||
|
||||
import styles from '../components/CompletionsTable.module.css'
|
||||
|
||||
export const meta: V2_MetaFunction = () => {
|
||||
return [{ title: 'Completions Review app' }, { name: 'description', content: 'Welcome to Completions Review app!' }]
|
||||
}
|
||||
|
||||
export const loader = async () => {
|
||||
return json({ completions: await getCompletions() })
|
||||
}
|
||||
|
||||
interface Column {
|
||||
Header: string
|
||||
accessor: string
|
||||
}
|
||||
|
||||
interface Row {
|
||||
code: string
|
||||
bgColor: string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const { completions } = useLoaderData<typeof loader>()
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
Header: 'Code',
|
||||
accessor: 'code',
|
||||
},
|
||||
]
|
||||
|
||||
// Dynamically generated columns based on the number of generated files.
|
||||
const extraColumns = new Set<string>()
|
||||
|
||||
const formatData = (inputData: typeof completions) => {
|
||||
const rows: Row[] = []
|
||||
|
||||
Object.entries(inputData).forEach(([code, entries]) => {
|
||||
const row: Row = {
|
||||
code: renderMarkdown(code),
|
||||
bgColor: '#f7f7f7',
|
||||
}
|
||||
|
||||
entries.forEach(({ timestamp, completions }) => {
|
||||
const columnKey = `completion-${timestamp}`
|
||||
|
||||
extraColumns.add(columnKey)
|
||||
|
||||
// All completions are displayed in the same cell separated by <hr />.
|
||||
row[columnKey] = completions.map(renderMarkdown).join('<hr />')
|
||||
})
|
||||
|
||||
rows.push(row)
|
||||
})
|
||||
|
||||
columns.push(...Array.from(extraColumns).map(columnKey => ({ Header: columnKey, accessor: columnKey })))
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
|
||||
columns,
|
||||
data: formatData(completions),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles['table-wrapper']}>
|
||||
<table {...getTableProps()} className={styles.table}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map(column => (
|
||||
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{rows.map(row => {
|
||||
prepareRow(row)
|
||||
|
||||
return (
|
||||
<tr {...row.getRowProps()} style={{ backgroundColor: row.original.bgColor }}>
|
||||
{row.cells.map(cell => {
|
||||
return (
|
||||
<td
|
||||
{...cell.getCellProps()}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: cell.value,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderMarkdown(code: string) {
|
||||
return marked(
|
||||
`\`\`\`javascript
|
||||
${code.replace(/\\/g, '\\\\')}
|
||||
\`\`\``,
|
||||
{ gfm: true }
|
||||
)
|
||||
}
|
||||
9
client/completions-review-tool/src/styles/global.css
Normal file
9
client/completions-review-tool/src/styles/global.css
Normal file
@ -0,0 +1,9 @@
|
||||
html {
|
||||
font-family: sfmono-regular, consolas, menlo, dejavu sans mono, monospace;
|
||||
-ms-scroll-chaining: none;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
14
client/completions-review-tool/tsconfig.json
Normal file
14
client/completions-review-tool/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"module": "commonjs",
|
||||
"sourceRoot": "src",
|
||||
"rootDir": ".",
|
||||
"outDir": "./out",
|
||||
"baseUrl": "./src",
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
// "noEmit": true,
|
||||
},
|
||||
"include": ["globals.d.ts", "remix.env.d.ts", "**/*.ts", "**/*.tsx", "*.js"],
|
||||
}
|
||||
@ -118,6 +118,7 @@
|
||||
"@pollyjs/adapter": "^5.0.0",
|
||||
"@pollyjs/core": "^5.1.0",
|
||||
"@pollyjs/persister-fs": "^5.0.0",
|
||||
"@remix-run/server-runtime": "^1.17.0",
|
||||
"@sentry/cli": "^1.74.4",
|
||||
"@sentry/webpack-plugin": "^1.20.0",
|
||||
"@slack/web-api": "^5.15.0",
|
||||
@ -508,6 +509,11 @@
|
||||
"node-gyp": "*"
|
||||
}
|
||||
},
|
||||
"deasync": {
|
||||
"dependencies": {
|
||||
"node-gyp": "*"
|
||||
}
|
||||
},
|
||||
"cpu-features": {
|
||||
"dependencies": {
|
||||
"node-gyp": "*"
|
||||
@ -521,6 +527,7 @@
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"browserify-zlib": "0.2.0",
|
||||
"@types/webpack": "5",
|
||||
"history": "4.5.1",
|
||||
"cssnano": "4.1.10",
|
||||
|
||||
1711
pnpm-lock.yaml
1711
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user