mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
refactor(web): build out feedback mechanisms (#2069)
This commit is contained in:
parent
bbcfb2b35e
commit
353de58647
71 changed files with 2053 additions and 189 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ build
|
|||
.DS_Store
|
||||
*.pdf
|
||||
node_modules
|
||||
mariadb_data
|
||||
|
||||
# Ignore direnv files
|
||||
.direnv/*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
ARG NODE_VERSION=slim
|
||||
# This Dockerfile is for the Harper website and web services.
|
||||
# You do not need it to use Harper.
|
||||
|
||||
ARG NODE_VERSION=24
|
||||
|
||||
FROM rust:latest AS wasm-build
|
||||
RUN rustup toolchain install
|
||||
|
|
@ -38,7 +41,10 @@ RUN pnpm build
|
|||
|
||||
FROM node:${NODE_VERSION}
|
||||
|
||||
COPY --from=node-build /usr/build/node_modules /usr/build/node_modules
|
||||
COPY --from=node-build /usr/build/packages/web/node_modules /usr/build/packages/web/node_modules
|
||||
COPY --from=node-build /usr/build/packages/web/build /usr/build/packages/web/build
|
||||
COPY ./packages/web/drizzle /usr/build/packages/web/build/drizzle
|
||||
COPY --from=node-build /usr/build/packages/web/package.json /usr/build/packages/web/package.json
|
||||
|
||||
WORKDIR /usr/build/packages/web/build
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"**/*.json",
|
||||
"!**/test-results",
|
||||
"!**/node_modules",
|
||||
"!**/mariadb_data",
|
||||
"!**/dist",
|
||||
"!**/target",
|
||||
"!**/build",
|
||||
|
|
|
|||
21
docker-compose.dev.yml
Normal file
21
docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# This Docker compose file is for development of the Harper website and web services.
|
||||
# You do not need it to use Harper.
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:lts
|
||||
restart: always
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: password
|
||||
MARIADB_DATABASE: harper
|
||||
MARIADB_USER: devuser
|
||||
MARIADB_PASSWORD: password
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./mariadb_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# This Docker compose file is for development of the Harper website and web services.
|
||||
# You do not need it to use Harper.
|
||||
|
||||
services:
|
||||
site:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- ORIGIN=http://localhost:3000
|
||||
- DATABASE_URL=mysql://devuser:password@db:3306/harper
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
db:
|
||||
image: mariadb:lts
|
||||
restart: always
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: password
|
||||
MARIADB_DATABASE: harper
|
||||
MARIADB_USER: devuser
|
||||
MARIADB_PASSWORD: password
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- ./mariadb_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
--color-accent-sand: #d68c45;
|
||||
}
|
||||
|
||||
@source "../node_modules/flowbite-svelte/dist";
|
||||
@source "./node_modules/flowbite-svelte/dist";
|
||||
|
||||
code {
|
||||
@apply bg-primary-100 rounded p-1;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
"@types/node": "catalog:",
|
||||
"flowbite": "^3.1.2",
|
||||
"flowbite-svelte": "^0.44.18",
|
||||
"flowbite-svelte-icons": "^2.1.1",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-zip": "^6.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
|
|
@ -48,12 +47,14 @@
|
|||
"vite": "^5.4.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@webcomponents/custom-elements": "^1.6.0",
|
||||
"harper.js": "workspace:*",
|
||||
"lint-framework": "workspace:*",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"svelte-fa": "^4.0.4",
|
||||
"tailwindcss": "^4.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
packages/chrome-plugin/src/PopupState.ts
Normal file
17
packages/chrome-plugin/src/PopupState.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export type PopupState =
|
||||
| {
|
||||
page: 'onboarding';
|
||||
}
|
||||
| {
|
||||
page: 'main';
|
||||
}
|
||||
| {
|
||||
page: 'report-error';
|
||||
feedback: string;
|
||||
example: string;
|
||||
rule_id: string;
|
||||
};
|
||||
|
||||
export function main(): PopupState {
|
||||
return { page: 'main' };
|
||||
}
|
||||
|
|
@ -95,8 +95,28 @@ export default class ProtocolClient {
|
|||
this.lintCache.clear();
|
||||
}
|
||||
|
||||
public static async openReportError(
|
||||
example: string,
|
||||
ruleId: string,
|
||||
feedback: string,
|
||||
): Promise<void> {
|
||||
await chrome.runtime.sendMessage({
|
||||
kind: 'openReportError',
|
||||
example,
|
||||
rule_id: ruleId,
|
||||
feedback,
|
||||
});
|
||||
}
|
||||
|
||||
public static async openOptions(): Promise<void> {
|
||||
// Use background to open options to support content scripts reliably
|
||||
await chrome.runtime.sendMessage({ kind: 'openOptions' });
|
||||
}
|
||||
|
||||
public static async postFormData(
|
||||
url: string,
|
||||
formData: Record<string, string>,
|
||||
): Promise<boolean> {
|
||||
return (await chrome.runtime.sendMessage({ kind: 'postFormData', url, formData })).success;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { BinaryModule, Dialect, type LintConfig, LocalLinter } from 'harper.js';
|
||||
import { type UnpackedLintGroups, unpackLint } from 'lint-framework';
|
||||
import type { PopupState } from '../PopupState';
|
||||
import {
|
||||
ActivationKey,
|
||||
type AddToUserDictionaryRequest,
|
||||
|
|
@ -20,6 +21,9 @@ import {
|
|||
type IgnoreLintRequest,
|
||||
type LintRequest,
|
||||
type LintResponse,
|
||||
type OpenReportErrorRequest,
|
||||
type PostFormDataRequest,
|
||||
type PostFormDataResponse,
|
||||
type Request,
|
||||
type Response,
|
||||
type SetActivationKeyRequest,
|
||||
|
|
@ -141,9 +145,13 @@ function handleRequest(message: Request): Promise<Response> {
|
|||
return handleGetActivationKey();
|
||||
case 'setActivationKey':
|
||||
return handleSetActivationKey(message);
|
||||
case 'openReportError':
|
||||
return handleOpenReportError(message);
|
||||
case 'openOptions':
|
||||
chrome.runtime.openOptionsPage();
|
||||
return createUnitResponse();
|
||||
return Promise.resolve(createUnitResponse());
|
||||
case 'postFormData':
|
||||
return handlePostFormData(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,9 +164,7 @@ async function handleLint(req: LintRequest): Promise<LintResponse> {
|
|||
const grouped = await linter.organizedLints(req.text);
|
||||
const unpackedEntries = await Promise.all(
|
||||
Object.entries(grouped).map(async ([source, lints]) => {
|
||||
const unpacked = await Promise.all(
|
||||
lints.map((lint) => unpackLint(req.text, lint, linter, source)),
|
||||
);
|
||||
const unpacked = await Promise.all(lints.map((lint) => unpackLint(req.text, lint, linter)));
|
||||
return [source, unpacked] as const;
|
||||
}),
|
||||
);
|
||||
|
|
@ -273,6 +279,46 @@ async function handleSetActivationKey(req: SetActivationKeyRequest): Promise<Uni
|
|||
return createUnitResponse();
|
||||
}
|
||||
|
||||
async function handleOpenReportError(req: OpenReportErrorRequest): Promise<UnitResponse> {
|
||||
const popupState: PopupState = {
|
||||
page: 'report-error',
|
||||
example: req.example,
|
||||
rule_id: req.rule_id,
|
||||
feedback: req.feedback,
|
||||
};
|
||||
|
||||
await chrome.storage.local.set({ popupState });
|
||||
|
||||
if (chrome.action?.openPopup) {
|
||||
try {
|
||||
await chrome.action.openPopup();
|
||||
} catch (error) {
|
||||
console.error('Failed to open popup for report error', error);
|
||||
}
|
||||
}
|
||||
|
||||
return createUnitResponse();
|
||||
}
|
||||
|
||||
async function handlePostFormData(req: PostFormDataRequest): Promise<PostFormDataResponse> {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(req.formData)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(req.url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return { kind: 'postFormData', success: response.ok };
|
||||
} catch (error) {
|
||||
console.error('Failed to post form data', error);
|
||||
return { kind: 'postFormData', success: false };
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the lint configuration inside the global `linter` and in permanent storage. */
|
||||
async function setLintConfig(lintConfig: LintConfig): Promise<void> {
|
||||
await linter.setLintConfig(lintConfig);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import '@webcomponents/custom-elements';
|
||||
import { isVisible, LintFramework, leafNodes } from 'lint-framework';
|
||||
import { isVisible, LintFramework, leafNodes, type UnpackedLint } from 'lint-framework';
|
||||
import isWordPress from '../isWordPress';
|
||||
import ProtocolClient from '../ProtocolClient';
|
||||
|
||||
|
|
@ -12,8 +12,23 @@ const fw = new LintFramework((text, domain) => ProtocolClient.lint(text, domain)
|
|||
getActivationKey: () => ProtocolClient.getActivationKey(),
|
||||
openOptions: () => ProtocolClient.openOptions(),
|
||||
addToUserDictionary: (words) => ProtocolClient.addToUserDictionary(words),
|
||||
reportError: (lint: UnpackedLint, ruleId: string) =>
|
||||
ProtocolClient.openReportError(
|
||||
padWithContext(lint.source, lint.span.start, lint.span.end, 15),
|
||||
ruleId,
|
||||
'',
|
||||
),
|
||||
});
|
||||
|
||||
function padWithContext(source: string, start: number, end: number, contextLength: number): string {
|
||||
const normalizedStart = Math.max(0, Math.min(start, source.length));
|
||||
const normalizedEnd = Math.max(normalizedStart, Math.min(end, source.length));
|
||||
const contextStart = Math.max(0, normalizedStart - contextLength);
|
||||
const contextEnd = Math.min(source.length, normalizedEnd + contextLength);
|
||||
|
||||
return source.slice(contextStart, contextEnd);
|
||||
}
|
||||
|
||||
const keepAliveCallback = () => {
|
||||
ProtocolClient.lint('', 'example.com');
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export function makeExtensionCSP(isDev: boolean): string {
|
|||
connectSrc.push('http://127.0.0.1:*', 'ws://127.0.0.1:*');
|
||||
}
|
||||
|
||||
connectSrc.push('https://writewithharper.com');
|
||||
|
||||
// Assemble the semicolon-delimited CSP
|
||||
return `${[
|
||||
`script-src ${scriptSrc.join(' ')}`,
|
||||
|
|
@ -73,4 +75,5 @@ export default defineManifest({
|
|||
content_security_policy: {
|
||||
extension_pages: makeExtensionCSP(isDev),
|
||||
},
|
||||
host_permissions: ['https://writewithharper.com/*'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ $effect(() => {
|
|||
*/
|
||||
export async function getCurrentTabDomain(): Promise<string | undefined> {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
console.log(tab);
|
||||
|
||||
if (!tab?.url) return undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Button } from 'flowbite-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Fa from 'svelte-fa';
|
||||
import logo from '/logo.png';
|
||||
import { main, type PopupState } from '../PopupState';
|
||||
import Main from './Main.svelte';
|
||||
import Onboarding from './Onboarding.svelte';
|
||||
import ReportProblematicLint from './ReportProblematicLint.svelte';
|
||||
|
||||
let page: 'onboarding' | 'main' = $state('main');
|
||||
let popupState: PopupState = $state({ page: 'main' });
|
||||
|
||||
$effect(() => {
|
||||
chrome.storage.local.get({ popupState: 'onboarding' }).then((result) => {
|
||||
page = result.popupState;
|
||||
chrome.storage.local.get({ popupState: { page: 'onboarding' } }).then((result) => {
|
||||
popupState = result.popupState;
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
chrome.storage.local.set({ popupState: page });
|
||||
chrome.storage.local.set({ popupState: $state.snapshot(popupState) });
|
||||
});
|
||||
|
||||
function openSettings() {
|
||||
|
|
@ -23,15 +26,25 @@ function openSettings() {
|
|||
</script>
|
||||
|
||||
<div class="w-[340px] border border-gray-200 bg-white font-sans flex flex-col rounded-lg shadow-sm select-none">
|
||||
<header class="flex items-center gap-2 px-3 py-2 bg-gray-50/60 rounded-t-lg">
|
||||
<img src={logo} alt="Harper logo" class="h-6 w-auto" />
|
||||
<span class="font-semibold text-sm">Harper</span>
|
||||
<header class="flex flex-row justify-between items-center gap-2 px-3 py-2 bg-gray-50/60 rounded-t-lg">
|
||||
<div class="flex flex-row justify-start items-center">
|
||||
<img src={logo} alt="Harper logo" class="h-6 w-auto" />
|
||||
<span class="font-semibold text-sm">Harper</span>
|
||||
</div>
|
||||
|
||||
{#if popupState.page != "main"}
|
||||
<Button outline on:click={() => {
|
||||
popupState = main();
|
||||
}}><Fa icon={faCaretLeft}/></Button>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
{#if page == "onboarding"}
|
||||
<Onboarding onConfirm={() => { page = "main";}} />
|
||||
{:else if page == "main"}
|
||||
{#if popupState.page == "onboarding"}
|
||||
<Onboarding onConfirm={() => { popupState = main();}} />
|
||||
{:else if popupState.page == "main"}
|
||||
<Main />
|
||||
{:else if popupState.page == 'report-error'}
|
||||
<ReportProblematicLint example={popupState.example} rule_id={popupState.rule_id} feedback={popupState.feedback} onSubmit={() => { popupState = main();}} />
|
||||
{/if}
|
||||
|
||||
<footer class="flex items-center justify-center gap-6 px-3 py-2 text-sm border-t border-gray-100 rounded-b-lg bg-white/60">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { Button, Checkbox, Input, Label } from 'flowbite-svelte';
|
||||
import ProtocolClient from '../ProtocolClient';
|
||||
|
||||
let {
|
||||
rule_id,
|
||||
feedback,
|
||||
example,
|
||||
onSubmit,
|
||||
}: { rule_id: string; feedback: string; example: string; onSubmit: () => void } = $props();
|
||||
|
||||
let submitting = $state(false);
|
||||
|
||||
async function handleSubmit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
submitting = true;
|
||||
|
||||
const success = await ProtocolClient.postFormData(
|
||||
'https://writewithharper.com/api/problematic-lints',
|
||||
{
|
||||
example,
|
||||
rule_id,
|
||||
feedback,
|
||||
is_false_positive: 'true',
|
||||
},
|
||||
);
|
||||
|
||||
submitting = false;
|
||||
|
||||
if (success) {
|
||||
onSubmit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="p-5">
|
||||
<h1 class="text-2xl font-semibold">Report Problematic Lint</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
Only the data you enter below will be sent to the Harper maintainer.
|
||||
</p>
|
||||
<form class="mt-4 space-y-6" onsubmit={handleSubmit}>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>What text caused (or should cause) feedback from Harper?</Label>
|
||||
</div>
|
||||
<Input name="example" bind:value={example} placeholder="Give us an example." />
|
||||
|
||||
<Checkbox name="is_false_positive" value="true" hidden />
|
||||
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>What rule caused (or should cause) feedback from Harper?</Label>
|
||||
</div>
|
||||
<Input
|
||||
name="rule_id"
|
||||
placeholder="We'd appreciate the specific rule ID, if applicable."
|
||||
bind:value={rule_id}
|
||||
/>
|
||||
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>Additional Feedback</Label>
|
||||
</div>
|
||||
<Input name="feedback" placeholder="Anything you want to add?" bind:value={feedback} />
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button type="submit" disabled={submitting}>Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,9 @@ export type Request =
|
|||
| GetUserDictionaryRequest
|
||||
| GetActivationKeyRequest
|
||||
| SetActivationKeyRequest
|
||||
| OpenOptionsRequest;
|
||||
| OpenOptionsRequest
|
||||
| OpenReportErrorRequest
|
||||
| PostFormDataRequest;
|
||||
|
||||
export type Response =
|
||||
| LintResponse
|
||||
|
|
@ -31,7 +33,8 @@ export type Response =
|
|||
| GetDefaultStatusResponse
|
||||
| GetEnabledDomainsResponse
|
||||
| GetUserDictionaryResponse
|
||||
| GetActivationKeyResponse;
|
||||
| GetActivationKeyResponse
|
||||
| PostFormDataResponse;
|
||||
|
||||
export type LintRequest = {
|
||||
kind: 'lint';
|
||||
|
|
@ -169,6 +172,11 @@ export type GetActivationKeyResponse = {
|
|||
key: ActivationKey;
|
||||
};
|
||||
|
||||
export type PostFormDataResponse = {
|
||||
kind: 'postFormData';
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
export type SetActivationKeyRequest = {
|
||||
kind: 'setActivationKey';
|
||||
key: ActivationKey;
|
||||
|
|
@ -177,3 +185,16 @@ export type SetActivationKeyRequest = {
|
|||
export type OpenOptionsRequest = {
|
||||
kind: 'openOptions';
|
||||
};
|
||||
|
||||
export type OpenReportErrorRequest = {
|
||||
kind: 'openReportError';
|
||||
example: string;
|
||||
rule_id: string;
|
||||
feedback: string;
|
||||
};
|
||||
|
||||
export type PostFormDataRequest = {
|
||||
kind: 'postFormData';
|
||||
url: string;
|
||||
formData: Record<string, string>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import computeLintBoxes from './computeLintBoxes';
|
|||
import { isVisible } from './domUtils';
|
||||
import Highlights from './Highlights';
|
||||
import PopupHandler from './PopupHandler';
|
||||
import type { UnpackedLintGroups } from './unpackLint';
|
||||
import type { UnpackedLint, UnpackedLintGroups } from './unpackLint';
|
||||
|
||||
type ActivationKey = 'off' | 'shift' | 'control';
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ export default class LintFramework {
|
|||
getActivationKey?: () => Promise<ActivationKey>;
|
||||
openOptions?: () => Promise<void>;
|
||||
addToUserDictionary?: (words: string[]) => Promise<void>;
|
||||
reportError?: (lint: UnpackedLint) => Promise<void>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
|
@ -41,6 +42,7 @@ export default class LintFramework {
|
|||
getActivationKey?: () => Promise<ActivationKey>;
|
||||
openOptions?: () => Promise<void>;
|
||||
addToUserDictionary?: (words: string[]) => Promise<void>;
|
||||
reportError?: () => Promise<void>;
|
||||
},
|
||||
) {
|
||||
this.lintProvider = lintProvider;
|
||||
|
|
@ -50,6 +52,7 @@ export default class LintFramework {
|
|||
getActivationKey: actions.getActivationKey,
|
||||
openOptions: actions.openOptions,
|
||||
addToUserDictionary: actions.addToUserDictionary,
|
||||
reportError: actions.reportError,
|
||||
});
|
||||
this.targets = new Set();
|
||||
this.scrollableAncestors = new Set();
|
||||
|
|
|
|||
|
|
@ -39,12 +39,14 @@ export default class PopupHandler {
|
|||
getActivationKey?: () => Promise<ActivationKey>;
|
||||
openOptions?: () => Promise<void>;
|
||||
addToUserDictionary?: (words: string[]) => Promise<void>;
|
||||
reportError?: () => Promise<void>;
|
||||
};
|
||||
|
||||
constructor(actions: {
|
||||
getActivationKey?: () => Promise<ActivationKey>;
|
||||
openOptions?: () => Promise<void>;
|
||||
addToUserDictionary?: (words: string[]) => Promise<void>;
|
||||
reportError?: () => Promise<void>;
|
||||
}) {
|
||||
this.actions = actions;
|
||||
this.currentLintBoxes = [];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import bookDownSvg from '../assets/bookDownSvg';
|
|||
import type { IgnorableLintBox, LintBox } from './Box';
|
||||
import lintKindColor from './lintKindColor';
|
||||
// Decoupled: actions passed in by framework consumer
|
||||
import type { UnpackedSuggestion } from './unpackLint';
|
||||
import type { UnpackedLint, UnpackedSuggestion } from './unpackLint';
|
||||
|
||||
var FocusHook: any = function () {};
|
||||
FocusHook.prototype.hook = function (node: any, _propertyName: any, _previousValue: any) {
|
||||
|
|
@ -185,6 +185,26 @@ function suggestions(
|
|||
});
|
||||
}
|
||||
|
||||
function reportProblemButton(reportError?: () => Promise<void>): any {
|
||||
if (!reportError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return h(
|
||||
'button',
|
||||
{
|
||||
className: 'harper-report-link',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
reportError();
|
||||
},
|
||||
title: 'Report an issue with this lint',
|
||||
'aria-label': 'Report an issue with this lint',
|
||||
},
|
||||
'Report',
|
||||
);
|
||||
}
|
||||
|
||||
function styleTag() {
|
||||
return h('style', { id: 'harper-suggestion-style' }, [
|
||||
`code{
|
||||
|
|
@ -341,6 +361,22 @@ function styleTag() {
|
|||
.harper-hint-drawer{ border-top-color:#30363d; background:#151b23; color:#9aa4af; }
|
||||
.harper-hint-icon{ background:#3a2f0b; color:#f2cc60; }
|
||||
.harper-hint-title{ color:#e6edf3; }
|
||||
}
|
||||
.harper-report-link{
|
||||
margin-top:8px;
|
||||
align-self:flex-start;
|
||||
background:none;
|
||||
border:none;
|
||||
padding:0;
|
||||
color:#0969da;
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
cursor:pointer;
|
||||
}
|
||||
.harper-report-link:hover{text-decoration:underline;}
|
||||
.harper-report-link:focus{outline:2px solid #0969da; outline-offset:2px;}
|
||||
@media (prefers-color-scheme:dark){
|
||||
.harper-report-link{color:#58a6ff;}
|
||||
}`,
|
||||
]);
|
||||
}
|
||||
|
|
@ -359,6 +395,7 @@ export default function SuggestionBox(
|
|||
actions: {
|
||||
openOptions?: () => Promise<void>;
|
||||
addToUserDictionary?: (words: string[]) => Promise<void>;
|
||||
reportError?: (lint: UnpackedLint, ruleId: string) => Promise<void>;
|
||||
},
|
||||
hint: string | null,
|
||||
close: () => void,
|
||||
|
|
@ -408,6 +445,14 @@ export default function SuggestionBox(
|
|||
],
|
||||
),
|
||||
hintDrawer(hint),
|
||||
actions.reportError
|
||||
? reportProblemButton(() => {
|
||||
if (actions.reportError) {
|
||||
return actions.reportError(box.lint, box.rule);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
: undefined,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
10
packages/web/drizzle.config.ts
Normal file
10
packages/web/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/lib/db/schema.ts',
|
||||
dialect: 'mysql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
6
packages/web/drizzle/0000_cute_zuras.sql
Normal file
6
packages/web/drizzle/0000_cute_zuras.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE `uninstall_feedback` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`feedback` text NOT NULL,
|
||||
`timestamp` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `uninstall_feedback_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
8
packages/web/drizzle/0001_blushing_corsair.sql
Normal file
8
packages/web/drizzle/0001_blushing_corsair.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE `problematic_lint` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`is_false_positive` boolean NOT NULL,
|
||||
`example` text NOT NULL,
|
||||
`feedback` text NOT NULL,
|
||||
`timestamp` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `problematic_lint_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
1
packages/web/drizzle/0002_blushing_chameleon.sql
Normal file
1
packages/web/drizzle/0002_blushing_chameleon.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `problematic_lint` ADD `rule_id` text;
|
||||
55
packages/web/drizzle/meta/0000_snapshot.json
Normal file
55
packages/web/drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "c5978949-00ad-4366-8af4-31ca63a60f87",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"uninstall_feedback": {
|
||||
"name": "uninstall_feedback",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"uninstall_feedback_id": {
|
||||
"name": "uninstall_feedback_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
106
packages/web/drizzle/meta/0001_snapshot.json
Normal file
106
packages/web/drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "a13c6522-4577-4493-a811-fc9a29305fcb",
|
||||
"prevId": "c5978949-00ad-4366-8af4-31ca63a60f87",
|
||||
"tables": {
|
||||
"problematic_lint": {
|
||||
"name": "problematic_lint",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"is_false_positive": {
|
||||
"name": "is_false_positive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"example": {
|
||||
"name": "example",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"problematic_lint_id": {
|
||||
"name": "problematic_lint_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"uninstall_feedback": {
|
||||
"name": "uninstall_feedback",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"uninstall_feedback_id": {
|
||||
"name": "uninstall_feedback_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
113
packages/web/drizzle/meta/0002_snapshot.json
Normal file
113
packages/web/drizzle/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "fbee3b57-d046-43fc-aa34-a2ef07e19993",
|
||||
"prevId": "a13c6522-4577-4493-a811-fc9a29305fcb",
|
||||
"tables": {
|
||||
"problematic_lint": {
|
||||
"name": "problematic_lint",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"is_false_positive": {
|
||||
"name": "is_false_positive",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"example": {
|
||||
"name": "example",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"rule_id": {
|
||||
"name": "rule_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"problematic_lint_id": {
|
||||
"name": "problematic_lint_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"uninstall_feedback": {
|
||||
"name": "uninstall_feedback",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"feedback": {
|
||||
"name": "feedback",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"uninstall_feedback_id": {
|
||||
"name": "uninstall_feedback_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
27
packages/web/drizzle/meta/_journal.json
Normal file
27
packages/web/drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "mysql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1760386452851,
|
||||
"tag": "0000_cute_zuras",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1760545628828,
|
||||
"tag": "0001_blushing_corsair",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1760546819156,
|
||||
"tag": "0002_blushing_chameleon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -12,10 +12,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/kit": "^2.17.1",
|
||||
"@sveltejs/kit": "^2.37.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/reveal.js": "^5.0.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"flowbite": "^3.1.2",
|
||||
"flowbite-svelte": "^0.44.18",
|
||||
"postcss": "^8.4.31",
|
||||
|
|
@ -23,9 +24,9 @@
|
|||
"svelte-check": "^4.1.5",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "catalog:",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "catalog:",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1"
|
||||
},
|
||||
|
|
@ -34,12 +35,16 @@
|
|||
"@sveltepress/theme-default": "^5.0.7",
|
||||
"@sveltepress/vite": "^1.1.5",
|
||||
"chart.js": "^4.4.8",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"harper.js": "workspace:*",
|
||||
"lint-framework": "workspace:*",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mysql2": "^3.15.2",
|
||||
"posthog-js": "^1.245.1",
|
||||
"reveal.js": "^5.1.0",
|
||||
"svelte-intersection-observer": "^1.0.0",
|
||||
"typed.js": "^2.1.0"
|
||||
"typed.js": "^2.1.0",
|
||||
"zod": "^4.1.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
packages/web/src/hooks.server.ts
Normal file
10
packages/web/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { db } from '$lib/db';
|
||||
import { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||
|
||||
// Migrate exactly once at startup
|
||||
try {
|
||||
await migrate(db, { migrationsFolder: './drizzle', migrationsTable: '__drizzle_migrations' });
|
||||
} catch (e: any) {
|
||||
console.log('Failed to migrate database.');
|
||||
console.error(e);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import LintCard from '$lib/LintCard.svelte';
|
||||
import LintCard from '$lib/components/LintCard.svelte';
|
||||
import { Card } from 'flowbite-svelte';
|
||||
import { type WorkerLinter } from 'harper.js';
|
||||
import {
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
type UnpackedSuggestion,
|
||||
unpackLint,
|
||||
} from 'lint-framework';
|
||||
import demo from '../../../../demo.md?raw';
|
||||
import demo from '../../../../../demo.md?raw';
|
||||
|
||||
export let content = demo.trim();
|
||||
|
||||
3
packages/web/src/lib/components/Isolate.svelte
Normal file
3
packages/web/src/lib/components/Isolate.svelte
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div class="fixed left-0 top-0 w-screen h-screen bg-white dark:bg-black z-1000">
|
||||
<slot />
|
||||
</div>
|
||||
3
packages/web/src/lib/db/index.ts
Normal file
3
packages/web/src/lib/db/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
|
||||
export const db = drizzle({ connection: { uri: process.env.DATABASE_URL } });
|
||||
20
packages/web/src/lib/db/models/ProblematicLints.ts
Normal file
20
packages/web/src/lib/db/models/ProblematicLints.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
||||
import { db } from '..';
|
||||
import { problematicLintTable } from '../schema';
|
||||
|
||||
export type ProblematicLintRow = typeof problematicLintTable.$inferSelect;
|
||||
const ProblematicLintRowParser = createSelectSchema(problematicLintTable);
|
||||
|
||||
export type ProblematicLintSubmission = typeof problematicLintTable.$inferInsert;
|
||||
const ProblematicLintSubmissionParser = createInsertSchema(problematicLintTable);
|
||||
|
||||
export default class ProblematicLints {
|
||||
public static async validateAndCreate(rec: any) {
|
||||
const parsed = ProblematicLintSubmissionParser.parse(rec);
|
||||
await this.create(parsed);
|
||||
}
|
||||
|
||||
public static async create(rec: ProblematicLintSubmission) {
|
||||
await db.insert(problematicLintTable).values(rec);
|
||||
}
|
||||
}
|
||||
20
packages/web/src/lib/db/models/UninstallFeedback.ts
Normal file
20
packages/web/src/lib/db/models/UninstallFeedback.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
||||
import { db } from '..';
|
||||
import { uninstallFeedbackTable } from '../schema';
|
||||
|
||||
export type UninstallFeedbackRow = typeof uninstallFeedbackTable.$inferSelect;
|
||||
const UninstallFeedbackRowParser = createSelectSchema(uninstallFeedbackTable);
|
||||
|
||||
export type UninstallFeedbackSubmission = typeof uninstallFeedbackTable.$inferInsert;
|
||||
const UninstallFeedbackSubmissionParser = createInsertSchema(uninstallFeedbackTable);
|
||||
|
||||
export default class UninstallFeedback {
|
||||
public static async validateAndCreate(rec: any) {
|
||||
const parsed = UninstallFeedbackSubmissionParser.parse(rec);
|
||||
await this.create(parsed);
|
||||
}
|
||||
|
||||
public static async create(rec: UninstallFeedbackSubmission) {
|
||||
await db.insert(uninstallFeedbackTable).values(rec);
|
||||
}
|
||||
}
|
||||
17
packages/web/src/lib/db/schema.ts
Normal file
17
packages/web/src/lib/db/schema.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { boolean, int, mysqlTable, text, timestamp } from 'drizzle-orm/mysql-core';
|
||||
|
||||
export const uninstallFeedbackTable = mysqlTable('uninstall_feedback', {
|
||||
id: int().autoincrement().primaryKey(),
|
||||
feedback: text().notNull(),
|
||||
timestamp: timestamp().notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const problematicLintTable = mysqlTable('problematic_lint', {
|
||||
id: int().autoincrement().primaryKey(),
|
||||
/** If false, implied to be a false-negative. */
|
||||
is_false_positive: boolean().notNull(),
|
||||
example: text().notNull(),
|
||||
feedback: text().notNull(),
|
||||
rule_id: text(),
|
||||
timestamp: timestamp().notNull().defaultNow(),
|
||||
});
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
import '../app.css';
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import AutomatticLogo from '$lib/AutomatticLogo.svelte';
|
||||
import GutterCenter from '$lib/GutterCenter.svelte';
|
||||
import AutomatticLogo from '$lib/components/AutomatticLogo.svelte';
|
||||
import GutterCenter from '$lib/components/GutterCenter.svelte';
|
||||
import posthog from 'posthog-js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,28 +5,33 @@ export const frontmatter = {
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ChromeLogo from '$lib/ChromeLogo.svelte';
|
||||
import CodeLogo from '$lib/CodeLogo.svelte';
|
||||
import Editor from '$lib/Editor.svelte';
|
||||
import FirefoxLogo from '$lib/FirefoxLogo.svelte';
|
||||
import GitHubLogo from '$lib/GitHubLogo.svelte';
|
||||
import ObsidianLogo from '$lib/ObsidianLogo.svelte';
|
||||
import Logo from '$lib/Logo.svelte';
|
||||
import Graph from '$lib/Graph.svelte';
|
||||
import Section from '$lib/Section.svelte';
|
||||
import EmacsLogo from '$lib/EmacsLogo.svelte';
|
||||
import HelixLogo from '$lib/HelixLogo.svelte';
|
||||
import NeovimLogo from '$lib/NeovimLogo.svelte';
|
||||
import SublimeLogo from '$lib/SublimeLogo.svelte';
|
||||
import WordPressLogo from '$lib/WordPressLogo.svelte';
|
||||
import ZedLogo from '$lib/ZedLogo.svelte';
|
||||
import EdgeLogo from '$lib/EdgeLogo.svelte';
|
||||
import ChromeLogo from '$lib/components/ChromeLogo.svelte';
|
||||
import CodeLogo from '$lib/components/CodeLogo.svelte';
|
||||
import Editor from '$lib/components/Editor.svelte';
|
||||
import FirefoxLogo from '$lib/components/FirefoxLogo.svelte';
|
||||
import GitHubLogo from '$lib/components/GitHubLogo.svelte';
|
||||
import ObsidianLogo from '$lib/components/ObsidianLogo.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import Graph from '$lib/components/Graph.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import EmacsLogo from '$lib/components/EmacsLogo.svelte';
|
||||
import HelixLogo from '$lib/components/HelixLogo.svelte';
|
||||
import NeovimLogo from '$lib/components/NeovimLogo.svelte';
|
||||
import SublimeLogo from '$lib/components/SublimeLogo.svelte';
|
||||
import WordPressLogo from '$lib/components/WordPressLogo.svelte';
|
||||
import ZedLogo from '$lib/components/ZedLogo.svelte';
|
||||
import EdgeLogo from '$lib/components/EdgeLogo.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
/**
|
||||
* @param {string} keyword
|
||||
*/
|
||||
function agentHas(keyword: string) {
|
||||
return navigator.userAgent.toLowerCase().search(keyword.toLowerCase()) > -1;
|
||||
function agentHas(keyword: string): boolean | undefined {
|
||||
if (!browser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return navigator.userAgent.toLowerCase().includes(keyword.toLowerCase());
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -88,7 +93,9 @@ function agentHas(keyword: string) {
|
|||
</div>
|
||||
|
||||
<div class="h-[800px] w-full overflow-hidden rounded-xl border border-neutral-200 shadow-sm dark:border-neutral-800">
|
||||
<Editor />
|
||||
{#if browser}
|
||||
<Editor />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
15
packages/web/src/routes/api/problematic-lints/+server.ts
Normal file
15
packages/web/src/routes/api/problematic-lints/+server.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import ProblematicLints from '$lib/db/models/ProblematicLints';
|
||||
import { type RequestEvent, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const POST = async ({ request }: RequestEvent) => {
|
||||
const data = await request.formData();
|
||||
|
||||
await ProblematicLints.validateAndCreate({
|
||||
is_false_positive: data.get('is_false_positive') === 'true',
|
||||
example: data.get('example'),
|
||||
rule_id: data.get('rule_id'),
|
||||
feedback: data.get('feedback'),
|
||||
});
|
||||
|
||||
throw redirect(303, '/');
|
||||
};
|
||||
11
packages/web/src/routes/api/uninstall-feedback/+server.ts
Normal file
11
packages/web/src/routes/api/uninstall-feedback/+server.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import UninstallFeedback from '$lib/db/models/UninstallFeedback';
|
||||
import { type RequestEvent, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const POST = async ({ request }: RequestEvent) => {
|
||||
const data = await request.formData();
|
||||
|
||||
await UninstallFeedback.validateAndCreate({
|
||||
feedback: data.get('feedback'),
|
||||
});
|
||||
throw redirect(303, '/');
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ Harper is a grammar checker designed to run anywhere there is text (so really, a
|
|||
Most Harper users are catching their mistakes in [Neovim](./integrations/neovim), [Obsidian](./integrations/obsidian), or [Visual Studio Code](./integrations/visual-studio-code).
|
||||
|
||||
<script>
|
||||
import Editor from "$lib/Editor.svelte"
|
||||
import Editor from "$lib/components/Editor.svelte"
|
||||
</script>
|
||||
|
||||
<div class="h-96">
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const prerender = true;
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
/// This page exists to be embedded via an `iframe`.
|
||||
import Editor from '$lib/Editor.svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import Editor from '$lib/components/Editor.svelte';
|
||||
import Isolate from '$lib/components/Isolate.svelte';
|
||||
|
||||
let content = $page.url.searchParams.get('initialText') ?? '';
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 w-full h-full z-[1000] bg-white dark:bg-black">
|
||||
<Isolate>
|
||||
<Editor {content}></Editor>
|
||||
</div>
|
||||
</Isolate>
|
||||
|
|
|
|||
1
packages/web/src/routes/editor/+page.ts
Normal file
1
packages/web/src/routes/editor/+page.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const ssr = false;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import 'reveal.js/dist/reveal.css';
|
||||
import 'reveal.js/dist/theme/serif.css';
|
||||
import Logo from '$lib/Logo.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import Reveal from 'reveal.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
|
|
|||
38
packages/web/src/routes/report-problematic-lint/+page.svelte
Normal file
38
packages/web/src/routes/report-problematic-lint/+page.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import Isolate from '$lib/components/Isolate.svelte';
|
||||
import { Button, Card, Checkbox, Input, Label, Radio } from 'flowbite-svelte';
|
||||
</script>
|
||||
|
||||
<Isolate>
|
||||
<div class="flex flex-row justify-center items-center h-screen">
|
||||
<Card>
|
||||
<h1 class="text-3xl font-semibold">Report Problematic Lint</h1>
|
||||
<p class="text-sm text-gray-600">If you've encountered an example of Harper producing an incorrect result, we'd love to know about it.</p>
|
||||
<form method="POST" class="mt-4 space-y-6" action="/api/problematic-lints">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>What text caused (or should cause) feedback from Harper?</Label>
|
||||
</div>
|
||||
<Input name="example" placeholder="Give us an example." />
|
||||
|
||||
<Checkbox name="is_false_positive">Is it a false positive? Otherwise, leave unchecked.</Checkbox>
|
||||
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>What rule caused (or should cause) feedback from Harper?</Label>
|
||||
</div>
|
||||
<Input name="rule_id" placeholder="We'd appreciate the specific rule ID, if applicable." />
|
||||
|
||||
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>Additional Feedback</Label>
|
||||
</div>
|
||||
<Input name="feedback" placeholder="Anything you want to add?" />
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</Isolate>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import LintKindChart from '$lib/LintKindChart.svelte';
|
||||
import LintKindChart from '$lib/components/LintKindChart.svelte';
|
||||
import {
|
||||
Fileupload,
|
||||
Table,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,54 @@
|
|||
<div class="fixed left-0 top-0 w-screen h-screen bg-white dark:bg-black z-1000">
|
||||
<div class="max-w-4xl mx-auto shadow-md border-gray-300 dark:border-x h-full">
|
||||
<iframe src="https://docs.google.com/forms/d/e/1FAIpQLSczTsdopx2AXpGiSemmYxuJAGoYBTKXLGWoORK3NpJKeHg3Vw/viewform?embedded=true" width="100%" height="100%" frameborder="0" marginheight="0" marginwidth="0">Loading…</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import Isolate from '$lib/components/Isolate.svelte';
|
||||
import { Button, Card, Input, Label, Radio } from 'flowbite-svelte';
|
||||
|
||||
const reasons = {
|
||||
confused: 'I was confused by how it worked',
|
||||
'unsupported-language': "It doesn't support my language",
|
||||
'slowed-down-browser': 'It slowed down my browser',
|
||||
'false-positive': 'It incorrectly flagged my text as an error',
|
||||
'false-negative': "It didn't identify enough errors in my text",
|
||||
'no-positives': "It didn't identify any errors in my text",
|
||||
};
|
||||
|
||||
let otherSelected: string | number | undefined;
|
||||
let otherText = '';
|
||||
|
||||
function handleFormData(e: FormDataEvent) {
|
||||
const fd = e.formData;
|
||||
if (fd.get('feedback') === 'other') {
|
||||
const v = (fd.get('other') || '').toString().trim();
|
||||
if (v) fd.set('feedback', v);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Isolate>
|
||||
<div class="flex flex-row justify-center items-center h-screen">
|
||||
<Card>
|
||||
<h1 class="text-3xl font-semibold">Uninstalling Harper</h1> <p class="text-sm text-gray-600">We’re sorry to see you go. If you have a minute, would you mind telling us why you uninstalled our browser extension?</p>
|
||||
<form method="POST" class="mt-4 space-y-6" action="/api/uninstall-feedback" on:formdata={handleFormData}>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<Label>Why did you uninstall Harper?</Label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each Object.entries(reasons) as [k, r], i}
|
||||
<Radio value={k} name="feedback">{r}</Radio>
|
||||
{/each}
|
||||
|
||||
<Radio name="feedback" value="other" bind:group={otherSelected}>Other</Radio>
|
||||
{#if otherSelected}
|
||||
<Input name="other" bind:value={otherText} placeholder="Your answer" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</Isolate>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|||
const config = {
|
||||
extensions: ['.svelte', '.md'],
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
csrf: {
|
||||
trustedOrigins: [
|
||||
'chrome-extension://lodbfhdipoipcjmlebjbgmmgekckhpfb',
|
||||
'chrome-extension://hkjdmakdmihopipoiplebkelbhebigea',
|
||||
],
|
||||
},
|
||||
prerender: {
|
||||
entries: [],
|
||||
},
|
||||
adapter: adapter({
|
||||
out: 'build',
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import { defineConfig } from 'vite';
|
|||
import topLevelAwait from 'vite-plugin-top-level-await';
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
|
||||
const prod = process.env.APP_ENV === 'production';
|
||||
|
||||
export default defineConfig({
|
||||
ssr: {
|
||||
noExternal: prod ? ['mysql2', 'drizzle-orm', 'posthog-js', 'drizzle-zod', 'zod'] : [],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
fs: {
|
||||
|
|
|
|||
1258
pnpm-lock.yaml
generated
1258
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue