diff --git a/.changeset/ten-trainers-accept.md b/.changeset/ten-trainers-accept.md new file mode 100644 index 00000000..11136d06 --- /dev/null +++ b/.changeset/ten-trainers-accept.md @@ -0,0 +1,8 @@ +--- +"@markprompt/docusaurus-theme-search": minor +"@markprompt/react": minor +"@markprompt/core": minor +"@markprompt/web": minor +--- + +Fix linting issues diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ada7bc6..7d2bddd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,4 +63,4 @@ jobs: with: build-script: '"build:packages"' pattern: '{./packages/**/dist/**/*.{cjs,js},./packages/css/markprompt.css}' - exclude: '{**/*.map,**/*.d.{ts,cts},**/node_modules/**,./packages/**/node_modules/**/dist/**/*}' + exclude: '{**/*.map,**/*.d.{ts,cts},**/node_modules/**,./packages/**/node_modules/**/dist/**/*,./packages/eslint-config/dist/**/*}' diff --git a/biome.jsonc b/biome.jsonc index 93efaa30..163d3588 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -46,8 +46,16 @@ "useDateNow": "error" }, "correctness": { + "noUndeclaredVariables": { + "level": "error" + // enable when biome@2.0.0 is released + // "options": { + // "checkTypes": true + // } + }, "noUnusedFunctionParameters": "error", "noUnusedImports": "error", + "noUnusedPrivateClassMembers": "error", "noUnusedVariables": "error", "useArrayLiterals": "error", "useHookAtTopLevel": "error" @@ -56,6 +64,7 @@ "noDefaultExport": "warn", "noNonNullAssertion": "warn", "useCollapsedElseIf": "error", + "useForOf": "error", "useThrowOnlyError": "error" }, "suspicious": { @@ -69,6 +78,15 @@ "useAwait": "error", "useErrorMessage": "error", "useNumberToFixedDigitsArgument": "error" + }, + "nursery": { + "noCommonJs": "error", + "noDuplicateElseIf": "error", + "noIrregularWhitespace": "error", + "noStaticElementInteractions": "error", + "useAdjacentOverloadSignatures": "error", + "useAriaPropsSupportedByRole": "error", + "useValidAutocomplete": "error" } } }, @@ -98,6 +116,32 @@ } } } + }, + { + // .d.ts files import types inside their declare, which isn't parsed correctly by Biome, yet. + "include": ["**/*.d.ts"], + "linter": { + "rules": { + "correctness": { + "noUndeclaredVariables": "off", + "noUnusedImports": "off" + } + } + } + }, + { + "include": [ + "examples/with-docusaurus*/sidebars.js", + "examples/with-docusaurus*/babel.config.js", + "examples/with-docusaurus*/docusaurus.config.ts" + ], + "linter": { + "rules": { + "nursery": { + "noCommonJs": "off" + } + } + } } ] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 332fbee8..d5889487 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,5 +13,4 @@ export default [ ], }, }, - ...configs.biome, ]; diff --git a/examples/with-function-calling/pages/index.tsx b/examples/with-function-calling/pages/index.tsx index fbd191f5..a6ef39b9 100644 --- a/examples/with-function-calling/pages/index.tsx +++ b/examples/with-function-calling/pages/index.tsx @@ -6,7 +6,7 @@ import { import Head from 'next/head'; import type { OpenAI } from 'openai'; import { useCallback, useState } from 'react'; -import type { JSX } from 'react'; +import type { FormEvent, JSX } from 'react'; interface ChatCompletionExecution { run?: (args: unknown) => void; @@ -44,7 +44,7 @@ export default function IndexPage(): JSX.Element { const [streamedMessage, setStreamedMessage] = useState(''); const submitForm = useCallback( - async (event: React.FormEvent) => { + async (event: FormEvent) => { event.preventDefault(); setStreamedMessage('Fetching response...'); @@ -92,7 +92,7 @@ export default function IndexPage(): JSX.Element { tool.run?.(toolCall.function?.arguments); setStreamedMessage( `Calling: ${tool.function.name}\n\nArguments:\n\n${ - toolCall.function?.arguments || {} + toolCall.function?.arguments || JSON.stringify({}) }`, ); } @@ -108,7 +108,12 @@ export default function IndexPage(): JSX.Element {
-
+ { + void submitForm(event); + }} + > diff --git a/examples/with-next/pages/_document.tsx b/examples/with-next/pages/_document.tsx index 3ba11dc4..ba4351f7 100644 --- a/examples/with-next/pages/_document.tsx +++ b/examples/with-next/pages/_document.tsx @@ -1,7 +1,8 @@ import { Html, Head, Main, NextScript } from 'next/document'; import Script from 'next/script'; +import type { JSX } from 'react'; -export default function Document(): React.JSX.Element { +export default function Document(): JSX.Element { return ( diff --git a/examples/with-next/pages/index.tsx b/examples/with-next/pages/index.tsx index efff0fcc..0506596a 100644 --- a/examples/with-next/pages/index.tsx +++ b/examples/with-next/pages/index.tsx @@ -7,13 +7,13 @@ import { SearchIcon } from '../components/icons'; export default function IndexPage(): JSX.Element { useEffect(() => { - const handleKeyDown = (event: KeyboardEvent): void => { + const handleKeyDown = async (event: KeyboardEvent): Promise => { if ( (event.key === 'k' && event.ctrlKey) || (event.key === 'k' && event.metaKey) ) { event.preventDefault(); - openMarkprompt(); + await openMarkprompt(); } }; diff --git a/examples/with-standalone-ticket-deflection/eslint.config.js b/examples/with-standalone-ticket-deflection/eslint.config.js index 45c4c8ad..579a7960 100644 --- a/examples/with-standalone-ticket-deflection/eslint.config.js +++ b/examples/with-standalone-ticket-deflection/eslint.config.js @@ -1,7 +1,3 @@ import { configs } from '@markprompt/eslint-config'; -export default [ - ...configs.base(import.meta.dirname), - ...configs.react, - ...configs.biome, -]; +export default [...configs.base(import.meta.dirname), ...configs.react]; diff --git a/package.json b/package.json index 0f3fd411..192cc9e5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "lint:css": "stylelint \"**/*.css\"", "lint:js": "eslint .", "lint:md": "remark . --frail", - "lint:ts": "pnpm build:packages", "lint": "turbo run lint:biome lint:css lint:js lint:md lint:ts", "postinstall": "manypkg check", "prepare": "husky", diff --git a/packages/core/eslint.config.js b/packages/core/eslint.config.js index 2e85b795..54eb1661 100644 --- a/packages/core/eslint.config.js +++ b/packages/core/eslint.config.js @@ -16,5 +16,4 @@ export default [ }, }, ...configs.vitest, - ...configs.biome, ]; diff --git a/packages/core/src/chat/index.test.ts b/packages/core/src/chat/index.test.ts index d3e75a5f..1b6bad8b 100644 --- a/packages/core/src/chat/index.test.ts +++ b/packages/core/src/chat/index.test.ts @@ -48,7 +48,7 @@ describe('submitChat', () => { let status = 200; const server = setupServer( - http.post(`${DEFAULT_OPTIONS.apiUrl!}/chat`, async ({ request }) => { + http.post(`${DEFAULT_OPTIONS.apiUrl}/chat`, async ({ request }) => { req = request; requestBody = (await request.json()) as SubmitChatOptions; @@ -75,7 +75,7 @@ describe('submitChat', () => { start(controller) { if (Array.isArray(response)) { let i = 0; - for (const chunk of response) { + for (const chunk of response as string[]) { controller.enqueue( encoder.encode( formatEvent({ @@ -356,7 +356,7 @@ describe('submitChat', () => { 'testKey', { stream: false }, )) { - await expect(json).toStrictEqual({ + expect(json).toStrictEqual({ content: 'According to my calculator 1 + 2 = 3', role: 'assistant', messageId, diff --git a/packages/core/src/chat/index.ts b/packages/core/src/chat/index.ts index e2e9d3ac..263fb206 100644 --- a/packages/core/src/chat/index.ts +++ b/packages/core/src/chat/index.ts @@ -111,7 +111,7 @@ export async function* submitChat( checkAbortSignal(options.signal); if (res.headers.get('Content-Type')?.includes('application/json')) { - const json = await res.json(); + const json: unknown = await res.json(); if ( isChatCompletion(json) && @@ -150,8 +150,13 @@ export async function* submitChat( const text = await res.text(); try { - const json = JSON.parse(text); - if (json.error) { + const json: unknown = JSON.parse(text); + if ( + json && + typeof json === 'object' && + 'error' in json && + typeof json.error === 'string' + ) { throw new Error(json.error); } } catch { @@ -182,7 +187,7 @@ export async function* submitChat( continue; } - const json = JSON.parse(event.data); + const json: unknown = JSON.parse(event.data); if (!isChatCompletionChunk(json)) { throw new Error('Malformed response from Markprompt API', { diff --git a/packages/core/src/chat/types.ts b/packages/core/src/chat/types.ts index 7de9d2b2..9a64e99f 100644 --- a/packages/core/src/chat/types.ts +++ b/packages/core/src/chat/types.ts @@ -53,7 +53,7 @@ export const COMPLETIONS_MODELS = [ export type CompletionsModel = ArrayToUnion; -export const EMBEDDINGS_MODEL = 'text-embedding-ada-002' as const; +export const EMBEDDINGS_MODEL = 'text-embedding-ada-002'; export type EmbeddingsModel = typeof EMBEDDINGS_MODEL; diff --git a/packages/core/src/chat/utils.ts b/packages/core/src/chat/utils.ts index ebe391c8..0d83c95e 100644 --- a/packages/core/src/chat/utils.ts +++ b/packages/core/src/chat/utils.ts @@ -59,14 +59,16 @@ export function checkAbortSignal(signal?: AbortSignal): void { throw signal.reason; } - throw new Error(signal.reason); + throw new Error( + typeof signal.reason === 'string' ? signal.reason : 'Aborted', + ); } } export const parseEncodedJSONHeader = ( response: Response, name: string, -): unknown | undefined => { +): unknown => { try { const headerValue = response.headers.get(name); if (headerValue) { diff --git a/packages/core/src/feedback.ts b/packages/core/src/feedback.ts index 9cd0ae90..9878d5a2 100644 --- a/packages/core/src/feedback.ts +++ b/packages/core/src/feedback.ts @@ -48,7 +48,7 @@ export async function submitFeedback( const resolvedOptions = defaults(cloneableOpts, { ...DEFAULT_OPTIONS, ...DEFAULT_SUBMIT_FEEDBACK_OPTIONS, - }); + }) as SubmitFeedbackOptions & BaseOptions; try { const response = await fetch( @@ -69,8 +69,21 @@ export async function submitFeedback( ); if (!response.ok) { - const error = (await response.json())?.error; - throw new Error(`Failed to submit feedback: ${error || 'Unknown error'}`); + const json: unknown = await response.json(); + if ( + json && + typeof json === 'object' && + 'error' in json && + typeof json.error === 'string' + ) { + throw new Error(`Failed to submit feedback: ${json.error}`, { + cause: json, + }); + } + + throw new Error(`Failed to submit feedback: 'Unknown error'`, { + cause: json, + }); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { @@ -111,7 +124,7 @@ export async function submitCSAT( const resolvedOptions = defaults(cloneableOpts, { ...DEFAULT_OPTIONS, ...DEFAULT_SUBMIT_FEEDBACK_OPTIONS, - }); + }) as SubmitFeedbackOptions & BaseOptions; try { const response = await fetch( @@ -129,8 +142,21 @@ export async function submitCSAT( ); if (!response.ok) { - const error = (await response.json())?.error; - throw new Error(`Failed to submit feedback: ${error || 'Unknown error'}`); + const json: unknown = await response.json(); + if ( + json && + typeof json === 'object' && + 'error' in json && + typeof json.error === 'string' + ) { + throw new Error(`Failed to submit feedback: ${json.error}`, { + cause: json, + }); + } + + throw new Error(`Failed to submit feedback: 'Unknown error'`, { + cause: json, + }); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { @@ -169,7 +195,7 @@ export async function submitCSATReason( const resolvedOptions = defaults(cloneableOpts, { ...DEFAULT_OPTIONS, ...DEFAULT_SUBMIT_FEEDBACK_OPTIONS, - }); + }) as SubmitFeedbackOptions & BaseOptions; try { const response = await fetch( @@ -187,8 +213,21 @@ export async function submitCSATReason( ); if (!response.ok) { - const error = (await response.json())?.error; - throw new Error(`Failed to submit feedback: ${error || 'Unknown error'}`); + const json: unknown = await response.json(); + if ( + json && + typeof json === 'object' && + 'error' in json && + typeof json.error === 'string' + ) { + throw new Error(`Failed to submit feedback: ${json.error}`, { + cause: json, + }); + } + + throw new Error(`Failed to submit feedback: 'Unknown error'`, { + cause: json, + }); } } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { diff --git a/packages/core/src/search.test.ts b/packages/core/src/search.test.ts index c33c0b5d..add3cdb9 100644 --- a/packages/core/src/search.test.ts +++ b/packages/core/src/search.test.ts @@ -81,7 +81,7 @@ let status = 200; let wait = 0; const server = setupServer( - http.get(`${DEFAULT_OPTIONS.apiUrl!}/search`, async ({ request }) => { + http.get(`${DEFAULT_OPTIONS.apiUrl}/search`, async ({ request }) => { const url = new URL(request.url); const limit = url.searchParams.get('limit'); let data = searchResults; diff --git a/packages/core/src/search.ts b/packages/core/src/search.ts index bf259876..e678f2bf 100644 --- a/packages/core/src/search.ts +++ b/packages/core/src/search.ts @@ -101,7 +101,7 @@ export async function submitSearchQuery( ...DEFAULT_OPTIONS, ...DEFAULT_SUBMIT_SEARCH_QUERY_OPTIONS, }, - ); + ) as SubmitSearchQueryOptions & BaseOptions; const params = new URLSearchParams({ query, @@ -128,7 +128,7 @@ export async function submitSearchQuery( ); } - return res.json(); + return (await res.json()) as SearchResultsResponse; } catch (error) { if (isAbortError(error)) { // do nothing on AbortError's, this is expected @@ -181,7 +181,7 @@ export async function submitAlgoliaDocsearchQuery( ); } - return res.json(); + return (await res.json()) as AlgoliaDocSearchResultsResponse; } catch (error) { if (isAbortError(error)) { // do nothing on AbortError's, this is expected diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 29c9b213..c4f8b263 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,3 +1,4 @@ +import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'; import type { FileSectionReference } from './types.js'; export type RequiredKeys = Required> & @@ -16,12 +17,22 @@ export const isKeyOf = ( export const getErrorMessage = async (res: Response): Promise => { const text = await res.text(); + try { - const json = JSON.parse(text); - return json?.error ?? text; + const json: unknown = JSON.parse(text); + if ( + json && + typeof json === 'object' && + 'error' in json && + typeof json.error === 'string' + ) { + return json?.error ?? text; + } } catch { return text; } + + return text; }; export function isAbortError(err: unknown): err is DOMException { @@ -36,7 +47,27 @@ export function isFileSectionReferences( ): data is FileSectionReference[] { return ( Array.isArray(data) && - Boolean(data[0]?.file?.path) && - Boolean(data[0]?.file?.source?.type) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + typeof data.at(0)?.file?.path === 'string' && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + typeof data.at(0)?.file?.source?.type === 'string' ); } + +export function getMessageTextContent(m: ChatCompletionMessageParam) { + if (!m.content) { + return; + } + + if (typeof m.content === 'string') { + return m.content; + } + + return m.content.reduce((acc, x) => { + if (x.type === 'text') { + return `${acc} ${x.text}`; + } + + return acc; + }, ''); +} diff --git a/packages/docusaurus-theme-search/eslint.config.js b/packages/docusaurus-theme-search/eslint.config.js index a9874235..fdcf118d 100644 --- a/packages/docusaurus-theme-search/eslint.config.js +++ b/packages/docusaurus-theme-search/eslint.config.js @@ -21,5 +21,4 @@ export default [ }, ...configs.react, ...configs.vitest, - ...configs.biome, ]; diff --git a/packages/docusaurus-theme-search/src/theme/SearchBar/index.tsx b/packages/docusaurus-theme-search/src/theme/SearchBar/index.tsx index 9f9b6174..17e36c2b 100644 --- a/packages/docusaurus-theme-search/src/theme/SearchBar/index.tsx +++ b/packages/docusaurus-theme-search/src/theme/SearchBar/index.tsx @@ -9,8 +9,20 @@ import { import { useEffect, useState } from 'react'; import type { JSX } from 'react'; +declare global { + interface Window { + markpromptConfigExtras?: { + references?: MarkpromptProps['references']; + search?: MarkpromptProps['search']; + }; + } +} + export default function SearchBar(): JSX.Element { - const [markpromptExtras, setMarkpromptExtras] = useState({}); + const [markpromptExtras, setMarkpromptExtras] = useState<{ + references?: MarkpromptProps['references']; + search?: MarkpromptProps['search']; + }>({}); const { siteConfig } = useDocusaurusContext(); useEffect(() => { @@ -18,7 +30,7 @@ export default function SearchBar(): JSX.Element { return; } - setMarkpromptExtras((window as any).markpromptConfigExtras || {}); + setMarkpromptExtras(window.markpromptConfigExtras || {}); }, []); const markpromptConfigProps = siteConfig.themeConfig @@ -38,6 +50,7 @@ export default function SearchBar(): JSX.Element { if (markpromptProps.trigger?.floating) { return ; } + return ( <>
@@ -47,9 +60,9 @@ export default function SearchBar(): JSX.Element { role="button" className="search-icon" onClick={() => openMarkprompt()} - onKeyDown={(event) => { + onKeyDown={async (event) => { if (event.key === 'Enter' || event.key === ' ') { - openMarkprompt(); + await openMarkprompt(); } }} tabIndex={0} diff --git a/packages/eslint-config/src/astro.ts b/packages/eslint-config/src/astro.ts index 898c9c5c..2871861a 100644 --- a/packages/eslint-config/src/astro.ts +++ b/packages/eslint-config/src/astro.ts @@ -3,7 +3,7 @@ import { configs } from 'eslint-plugin-astro'; export const astro: Linter.Config[] = [ ...configs['flat/recommended'], - ...configs['jsx-a11y-recommended'], + ...configs['flat/jsx-a11y-recommended'], { files: ['**/*.astro'], settings: { @@ -12,7 +12,14 @@ export const astro: Linter.Config[] = [ 'astro-eslint-parser': ['.astro'], }, }, + languageOptions: { + parserOptions: { + projectService: false, + }, + }, rules: { + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/no-misused-promises': 'off', 'import-x/default': 'off', }, }, diff --git a/packages/eslint-config/src/base.ts b/packages/eslint-config/src/base.ts index 2c2efbcc..ae30a219 100644 --- a/packages/eslint-config/src/base.ts +++ b/packages/eslint-config/src/base.ts @@ -1,44 +1,22 @@ -import js from '@eslint/js'; -import type { Linter } from 'eslint'; -import turbo from 'eslint-config-turbo/flat'; +import type { ESLint, Linter } from 'eslint'; import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'; import importX from 'eslint-plugin-import-x'; import promise from 'eslint-plugin-promise'; import globals from 'globals'; -import ts, { parser } from 'typescript-eslint'; +import tseslint from 'typescript-eslint'; +import turbo from 'eslint-plugin-turbo'; -export const base = ( - rootDir: string, - allowDefaultProject?: string[], +export const base = ( + _rootDir: T, + _allowDefaultProject?: string[], ): Linter.Config[] => [ { ignores: ['.turbo/', 'dist/'], }, - js.configs.recommended, - promise.configs['flat/recommended'], - // eslint-disable-next-line import-x/no-named-as-default-member - ...(ts.configs.recommended as Linter.Config[]), - // eslint-disable-next-line import-x/no-named-as-default-member - ...(ts.configs.stylistic as Linter.Config[]), - importX.flatConfigs.recommended as Linter.Config, - importX.flatConfigs.typescript, - ...turbo, + + // eslint/js config and rules { - files: ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'], languageOptions: { - parser: parser as Linter.Parser, - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - tsconfigRootDir: rootDir, - projectService: allowDefaultProject - ? { - allowDefaultProject, - } - : true, - warnOnUnsupportedTypeScriptVersion: false, - }, ecmaVersion: 'latest', sourceType: 'module', globals: { @@ -46,7 +24,88 @@ export const base = ( ...globals.node, }, }, + }, + { + // only enable @eslint/js rules that Biome doesn't cover + rules: { + 'no-constant-binary-expression': 'error', + 'no-delete-var': 'error', + 'no-invalid-regexp': 'error', + 'no-octal': 'error', + 'no-unexpected-multiline': 'error', + 'no-useless-backreference': 'error', + }, + }, + + // typescript-eslint config and rules + { + plugins: { + '@typescript-eslint': tseslint.plugin as ESLint.Plugin, + }, + languageOptions: { + parser: tseslint.parser as Linter.Parser, + parserOptions: { + // tsconfigRootDir: rootDir, + // projectService: allowDefaultProject + // ? { + // allowDefaultProject, + // } + // : true, + warnOnUnsupportedTypeScriptVersion: false, + }, + }, + }, + { + // only enable @typescript-eslint rules that Biome doesn't cover + rules: { + // '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/class-literal-property-style': 'error', + '@typescript-eslint/consistent-generic-constructors': 'error', + '@typescript-eslint/consistent-indexed-object-style': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + '@typescript-eslint/no-confusing-non-null-assertion': 'error', + '@typescript-eslint/no-duplicate-enum-values': 'error', + '@typescript-eslint/no-empty-object-type': 'error', + // '@typescript-eslint/no-misused-promises': [ + // 'error', + // { checksVoidReturn: false }, + // ], + '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', + '@typescript-eslint/no-unsafe-function-type': 'error', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', + '@typescript-eslint/triple-slash-reference': 'error', + }, + }, + + // promise config and rules + promise.configs['flat/recommended'], + + { + plugins: importX.flatConfigs.recommended.plugins as Record< + string, + ESLint.Plugin + >, settings: { + 'import-x/extensions': [ + '.js', + '.jsx', + '.cjs', + '.mjs', + '.ts', + '.tsx', + '.cts', + '.mts', + ], + 'import-x/external-module-folders': [ + 'node_modules', + 'node_modules/@types', + ], + 'import-x/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx', '.cts', '.mts'], + }, 'import-x/resolver-next': [ createTypeScriptImportResolver({ alwaysTryTypes: true, @@ -55,11 +114,12 @@ export const base = ( }, }, { + // https://typescript-eslint.io/troubleshooting/typed-linting/performance#eslint-plugin-import rules: { - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - '@typescript-eslint/consistent-indexed-object-style': ['error', 'record'], - // https://github.com/import-js/eslint-plugin-import/issues/2340 - 'import-x/namespace': 'off', + 'import-x/no-named-as-default': 'error', + 'import-x/no-cycle': 'error', + 'import-x/no-unused-modules': 'error', + 'import-x/no-deprecated': 'error', 'import-x/order': [ 'error', { @@ -88,4 +148,13 @@ export const base = ( ], }, }, + + { + plugins: { + turbo: turbo, + }, + rules: { + 'turbo/no-undeclared-env-vars': 'error', + }, + }, ]; diff --git a/packages/eslint-config/src/biome.ts b/packages/eslint-config/src/biome.ts deleted file mode 100644 index c9522ace..00000000 --- a/packages/eslint-config/src/biome.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { Linter } from 'eslint'; - -export const biome: Linter.Config[] = [ - // these rules have been superseded by Biome - { - name: 'biome', - rules: { - // eslint builtin rules - 'constructor-super': 'off', - 'default-case-last': 'off', - 'default-param-last': 'off', - 'dot-notation': 'off', - eqeqeq: 'off', - 'for-direction': 'off', - 'getter-return': 'off', - 'no-array-constructor': 'off', - 'no-async-promise-executor': 'off', - 'no-case-declarations': 'off', - 'no-class-assign': 'off', - 'no-compare-neg-zero': 'off', - 'no-cond-assign': 'off', - 'no-const-assign': 'off', - 'no-constant-condition': 'off', - 'no-constructor-return': 'off', - 'no-control-regex': 'off', - 'no-debugger': 'off', - 'no-dupe-args': 'off', - 'no-dupe-class-members': 'off', - 'no-dupe-keys': 'off', - 'no-duplicate-case': 'off', - 'no-else-return': 'off', - 'no-empty-character-class': 'off', - 'no-empty-function': 'off', - 'no-empty-pattern': 'off', - 'no-empty-static-block': 'off', - 'no-empty': 'off', - 'no-eval': 'off', - 'no-ex-assign': 'off', - 'no-extra-boolean-cast': 'off', - 'no-extra-label': 'off', - 'no-fallthrough': 'off', - 'no-func-assign': 'off', - 'no-global-assign': 'off', - 'no-import-assign': 'off', - 'no-inner-declarations': 'off', - 'no-label-var': 'off', - 'no-labels': 'off', - 'no-lone-blocks': 'off', - 'no-loss-of-precision': 'off', - 'no-misleading-character-class': 'off', - 'no-new-native-nonconstructor': 'off', - 'no-nonoctal-decimal-escape': 'off', - 'no-obj-calls': 'off', - 'no-param-reassign': 'off', - 'no-prototype-builtins': 'off', - 'no-redeclare': 'off', - 'no-regex-spaces': 'off', - 'no-self-assign': 'off', - 'no-self-compare': 'off', - 'no-sequences': 'off', - 'no-setter-return': 'off', - 'no-shadow-restricted-names': 'off', - 'no-sparse-arrays': 'off', - 'no-this-before-super': 'off', - 'no-undef-init': 'off', - 'no-unneeded-ternary': 'off', - 'no-unreachable': 'off', - 'no-unsafe-finally': 'off', - 'no-unsafe-negation': 'off', - 'no-unsafe-optional-chaining': 'off', - 'no-unused-labels': 'off', - 'no-unused-vars': 'off', - 'no-use-before-define': 'off', - 'no-useless-catch': 'off', - 'no-useless-concat': 'off', - 'no-useless-rename': 'off', - 'no-var': 'off', - 'no-with': 'off', - 'one-var': 'off', - 'prefer-arrow-callback': 'off', - 'prefer-const': 'off', - 'prefer-exponentiation-operator': 'off', - 'prefer-numeric-literals': 'off', - 'prefer-regex-literals': 'off', - 'prefer-rest-params': 'off', - 'prefer-template': 'off', - 'require-await': 'off', - 'require-yield': 'off', - 'use-isnan': 'off', - 'valid-typeof': 'off', - - // react/react-hooks rules - 'react/jsx-key': 'off', - 'react/jsx-no-comment-textnodes': 'off', - 'react/jsx-no-duplicate-props': 'off', - 'react/jsx-no-useless-fragment': 'off', - 'react/no-array-index-key': 'off', - 'react/no-children-prop': 'off', - 'react/prop-types': 'off', - 'react/void-dom-elements-no-children': 'off', - - // typescript-eslint rules - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/consistent-type-exports': 'off', - '@typescript-eslint/consistent-type-imports': 'off', - '@typescript-eslint/default-param-last': 'off', - '@typescript-eslint/dot-notation': 'off', - '@typescript-eslint/no-dupe-class-members': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-extra-non-null-assertion': 'off', - '@typescript-eslint/no-extraneous-class': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-invalid-void-type': 'off', - '@typescript-eslint/no-loss-of-precision': 'off', - '@typescript-eslint/no-misused-new': 'off', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-redeclare': 'off', - '@typescript-eslint/no-this-alias': 'off', - '@typescript-eslint/no-unnecessary-type-constraint': 'off', - '@typescript-eslint/no-unsafe-declaration-merging': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/no-useless-constructor': 'off', - '@typescript-eslint/no-useless-empty-export': 'off', - '@typescript-eslint/prefer-as-const': 'off', - '@typescript-eslint/prefer-enum-initializers': 'off', - '@typescript-eslint/prefer-for-of': 'off', - '@typescript-eslint/prefer-function-type': 'off', - '@typescript-eslint/prefer-literal-enum-member': 'off', - '@typescript-eslint/prefer-namespace-keyword': 'off', - '@typescript-eslint/prefer-optional-chain': 'off', - '@typescript-eslint/require-await': 'off', - }, - }, -]; diff --git a/packages/eslint-config/src/index.ts b/packages/eslint-config/src/index.ts index 9bc836be..00b3c713 100644 --- a/packages/eslint-config/src/index.ts +++ b/packages/eslint-config/src/index.ts @@ -1,6 +1,5 @@ import { astro } from './astro.js'; import { base } from './base.js'; -import { biome } from './biome.js'; import { next } from './next.js'; import { react } from './react.js'; import { tanstack } from './tanstack.js'; @@ -9,7 +8,6 @@ import { vitest } from './vitest.js'; export const configs = { astro: astro, base: base, - biome: biome, next: next, react: react, tanstack: tanstack, diff --git a/packages/eslint-config/src/react.ts b/packages/eslint-config/src/react.ts index 041cde2e..b2301a35 100644 --- a/packages/eslint-config/src/react.ts +++ b/packages/eslint-config/src/react.ts @@ -1,49 +1,123 @@ import type { ESLint, Linter } from 'eslint'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; import pluginReact from 'eslint-plugin-react'; -import pluginReactRefresh from 'eslint-plugin-react-refresh'; -import globals from 'globals'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; -export const react = [ +export const react: Linter.Config[] = [ { name: 'react', - ...pluginReact.configs.flat.recommended, - ...pluginReact.configs.flat['jsx-runtime'], - plugins: { - react: pluginReact as ESLint.Plugin, - 'react-refresh': pluginReactRefresh, - }, + plugins: pluginReact.configs.flat.recommended.plugins, languageOptions: { - ...pluginReact.configs.flat.recommended.languageOptions, - ...pluginReact.configs.flat['jsx-runtime'].languageOptions, - globals: { - ...globals.browser, - ...globals.node, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + jsxPragma: null, // for @typescript/eslint-parser }, }, settings: { react: { - version: '18', + version: 'detect', }, + formComponents: ['Form'], + linkComponents: [ + 'Link', + 'LinkButton', + 'TanstackLinkButton', + 'TanstackTextLink', + 'TextLink', + ], + }, + }, + { + rules: { + 'react/display-name': 'error', + 'react/jsx-no-target-blank': 'error', + 'react/jsx-no-undef': 'error', + 'react/jsx-uses-vars': 'error', + 'react/no-deprecated': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'error', + 'react/no-is-mounted': 'error', + 'react/no-render-return-value': 'error', + 'react/no-string-refs': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unknown-property': 'error', + 'react/no-unsafe': 'off', + 'react/prop-types': 'error', + 'react/require-render-return': 'error', + // for automatic jsx-runtime + 'react/react-in-jsx-scope': 'off', + 'react/jsx-uses-react': 'off', + }, + }, + + { + plugins: { + 'react-refresh': reactRefresh, }, rules: { - ...pluginReact.configs.flat.recommended.rules, - ...pluginReact.configs.flat['jsx-runtime'].rules, 'react-refresh/only-export-components': [ 'error', + // technically only for Vite apps, but most of our apps are. { allowConstantExport: true }, ], }, }, + { - files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], - ...jsxA11y.flatConfigs.recommended, - languageOptions: { - ...jsxA11y.flatConfigs.recommended.languageOptions, - globals: { - ...globals.browser, - ...globals.node, - }, + plugins: { + 'jsx-a11y': jsxA11y as ESLint.Plugin, + }, + rules: { + 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error + 'jsx-a11y/control-has-associated-label': [ + 'off', + { + ignoreElements: [ + 'audio', + 'canvas', + 'embed', + 'input', + 'textarea', + 'tr', + 'video', + ], + ignoreRoles: [ + 'grid', + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'row', + 'tablist', + 'toolbar', + 'tree', + 'treegrid', + ], + includeRoles: ['alert', 'dialog'], + }, + ], + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'error', + { + handlers: [ + 'onClick', + 'onError', + 'onLoad', + 'onMouseDown', + 'onMouseUp', + 'onKeyPress', + 'onKeyDown', + 'onKeyUp', + ], + alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + body: ['onError', 'onLoad'], + dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], + }, + ], }, }, -] satisfies Linter.Config[]; +]; diff --git a/packages/eslint-config/src/types/eslint-config-turbo.d.ts b/packages/eslint-config/src/types/eslint-config-turbo.d.ts index 4207005d..0d8d762f 100644 --- a/packages/eslint-config/src/types/eslint-config-turbo.d.ts +++ b/packages/eslint-config/src/types/eslint-config-turbo.d.ts @@ -1,6 +1,8 @@ declare module 'eslint-config-turbo/flat' { // biome-ignore lint/correctness/noUnusedImports: this is correct in d.ts files import type { Linter } from 'eslint'; + // biome-ignore lint/correctness/noUndeclaredVariables: false positive const config: Linter.Config[]; + export = config; } diff --git a/packages/eslint-config/src/types/eslint-plugin-jsx-a11y.d.ts b/packages/eslint-config/src/types/eslint-plugin-jsx-a11y.d.ts index 5b784e48..922201b9 100644 --- a/packages/eslint-config/src/types/eslint-plugin-jsx-a11y.d.ts +++ b/packages/eslint-config/src/types/eslint-plugin-jsx-a11y.d.ts @@ -1,5 +1,4 @@ declare module 'eslint-plugin-jsx-a11y' { - // biome-ignore lint/correctness/noUnusedImports: this is correct in d.ts files import type { Linter } from 'eslint'; interface Plugin { diff --git a/packages/react/eslint.config.js b/packages/react/eslint.config.js index b77c4f99..99a7dbac 100644 --- a/packages/react/eslint.config.js +++ b/packages/react/eslint.config.js @@ -5,9 +5,6 @@ export default [ 'eslint.config.js', 'vitest.config.ts', 'vitest.setup.ts', - 'src/*.test.ts', - 'src/*/*.test.ts', - '__mocks__/*.ts', ]), { rules: { @@ -19,5 +16,4 @@ export default [ }, ...configs.react, ...configs.vitest, - ...configs.biome, ]; diff --git a/packages/react/package.json b/packages/react/package.json index 40b29b9f..173cf2c8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -28,7 +28,7 @@ "files": ["dist"], "scripts": { "build": "tsc --build tsconfig.build.json", - "dev": "tsc --build tsconfig.build.json --watch", + "dev": "tsc --build tsconfig.json --watch", "lint:js": "eslint .", "lint:ts": "tsc --build --noEmit", "prepack": "tsc --build tsconfig.build.json" @@ -59,9 +59,9 @@ "zustand": "^4.5.5" }, "devDependencies": { - "@testing-library/dom": "^10.0.0", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^15.0.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", "@types/lodash-es": "^4.17.12", "@types/react": "^19.0.1", diff --git a/packages/react/src/CreateTicketView.tsx b/packages/react/src/CreateTicketView.tsx index 364f7f23..d202cd07 100644 --- a/packages/react/src/CreateTicketView.tsx +++ b/packages/react/src/CreateTicketView.tsx @@ -5,6 +5,7 @@ import { shift, useFloating, } from '@floating-ui/react-dom'; +import { getMessageTextContent } from '@markprompt/core/utils'; import { AccessibleIcon } from '@radix-ui/react-accessible-icon'; import { clsx } from 'clsx'; import { useSelect } from 'downshift'; @@ -19,14 +20,12 @@ import { type JSX, } from 'react'; +import { useChatStore, type ChatViewMessage } from './chat/store.js'; import { toValidApiMessages } from './chat/utils.js'; import { useGlobalStore } from './context/global/store.js'; import { ChevronDownIcon, ChevronLeftIcon, LoadingIcon } from './icons.js'; -import { - useChatStore, - type ChatViewMessage, - type CustomField, -} from './index.js'; +import type { CustomField } from './types.js'; +import { isPresent } from './utils.js'; export interface CreateTicketViewProps { handleGoBack: () => void; @@ -77,43 +76,39 @@ export function CreateTicketView(props: CreateTicketViewProps): JSX.Element { ): Promise => { event.preventDefault(); - if ( - !apiUrl || - !projectKey || - !provider || - !event.currentTarget.email.value || - !event.currentTarget.userName.value || - !event.currentTarget.summary.value - ) { + if (!apiUrl || !projectKey || !provider) { return; } - setResult(undefined); - setSubmittingCase(true); - try { const data = new FormData(event.currentTarget); - if (threadId) { - data.set('threadId', threadId); + + if (!data.get('email') || !data.get('userName') || !data.get('summary')) { + return; } - const files = data.get('files'); - const requestBody = - files && (files as any).size > 0 - ? { - method: 'POST', - // don't pass a Content-Type header here, the browser will - // generate a correct header which includes the boundary. - body: data, - headers, - } - : { - method: 'POST', - body: JSON.stringify(Object.fromEntries(data.entries())), - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - }; + + setResult(undefined); + setSubmittingCase(true); + + const files = data.getAll('files') as File[]; + + const requestBody = files?.some((f) => f.size > 0) + ? { + method: 'POST', + // don't pass a Content-Type header here, the browser will + // generate a correct header which includes the boundary. + body: data, + headers, + } + : { + method: 'POST', + body: JSON.stringify(Object.fromEntries(data.entries())), + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + }; + // copy a field for legacy reasons const result = await fetch( `${apiUrl}/integrations/create-ticket?projectKey=${projectKey}`, @@ -440,8 +435,11 @@ function getFullSummaryData( ): { subject: string; body: string } { const transcript = `Full transcript:\n\n${toValidApiMessages(messages) .map((m) => { - return `${m.role === 'user' ? 'Me' : 'AI'}: ${m.content}`; + const content = getMessageTextContent(m); + if (!content) return; + return `${m.role === 'user' ? 'Me' : 'AI'}: ${content}`; }) + .filter(isPresent) .join('\n\n')}`; let subject = ''; @@ -449,9 +447,12 @@ function getFullSummaryData( if (summary?.content) { try { - const data = JSON.parse(summary.content); + const data = JSON.parse(summary.content) as { + subject: string; + fullSummary?: string; + }; subject = data.subject; - body = `${data.fullSummary || ''}\n\n---\n\n${transcript}`; + body = `${data.fullSummary ?? ''}\n\n---\n\n${transcript}`; } catch { // Do nothing } diff --git a/packages/react/src/Markprompt.test.tsx b/packages/react/src/Markprompt.test.tsx index 4549f8a3..519e7d1d 100644 --- a/packages/react/src/Markprompt.test.tsx +++ b/packages/react/src/Markprompt.test.tsx @@ -13,10 +13,10 @@ describe('Markprompt', () => { // Before re-enabling this test, we should review the keyboard shortcuts. it.skip('opens the dialog when a hotkey is pressed while the non-floating trigger is rendered', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); await user.keyboard('{Meta>}{Enter}{/Meta}'); - await expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); }); it('renders no dialog when display = plain', () => { @@ -42,7 +42,7 @@ describe('Markprompt', () => { }); it('renders search view when search is enabled', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { }); it('renders chat view when chat is enabled', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); await user.click(screen.getByText('Ask AI')); expect(screen.getByText('Chats')).toBeInTheDocument(); }); it('renders tabs when multiple views are enabled', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { await user.click(screen.getByText('Ask AI')); // tabs are rendered - await expect(screen.getByText('searchtab')).toBeInTheDocument(); - await expect(screen.getByText('chattab')).toBeInTheDocument(); + expect(screen.getByText('searchtab')).toBeInTheDocument(); + expect(screen.getByText('chattab')).toBeInTheDocument(); // wait for lazy loaded content await screen.findByRole('searchbox'); // tabs switching await user.click(screen.getByText('chattab')); - await expect(screen.getByRole('textbox')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); }); it('renders the title and description visually hidden', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); const { rerender } = render( { }); it('calls back on open', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); const fn = vi.fn(); render(); await user.click(screen.getByText('Ask AI')); @@ -149,7 +149,7 @@ describe('Markprompt', () => { it('opens programmatically', async () => { const fn = vi.fn(); render(); - act(() => openMarkprompt()); + await act(() => openMarkprompt()); await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument(); }); @@ -161,12 +161,12 @@ describe('Markprompt', () => { render(); - act(() => openMarkprompt()); + await act(() => openMarkprompt()); await waitFor(() => { expect(screen.getByRole('dialog')).toBeInTheDocument(); }); - act(() => closeMarkprompt()); + await act(() => closeMarkprompt()); await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); diff --git a/packages/react/src/Markprompt.tsx b/packages/react/src/Markprompt.tsx index 76121a0b..e589643c 100644 --- a/packages/react/src/Markprompt.tsx +++ b/packages/react/src/Markprompt.tsx @@ -13,12 +13,13 @@ import { } from 'react'; import { ChatView } from './chat/ChatView.js'; +import { ChatProvider } from './chat/provider.js'; +import { useChatStore } from './chat/store.js'; import { DEFAULT_MARKPROMPT_OPTIONS } from './constants.js'; import { GlobalStoreProvider } from './context/global/provider.js'; import { useGlobalStore } from './context/global/store.js'; import { CreateTicketView } from './CreateTicketView.js'; import { CloseIcon, SparklesIcon } from './icons.js'; -import { ChatProvider, useChatStore } from './index.js'; import { Menu } from './Menu.js'; import * as BaseMarkprompt from './primitives/headless.js'; import { TicketDeflectionForm } from './TicketDeflectionForm.js'; @@ -184,8 +185,8 @@ function Markprompt(props: MarkpromptProps): JSX.Element { }; }, [display, onDidRequestOpenChange]); - const onTriggerClicked = useCallback(() => { - openMarkprompt(menu ? 'menu' : 'chat'); + const onTriggerClicked = useCallback(async () => { + await openMarkprompt(menu ? 'menu' : 'chat'); }, [menu]); return ( diff --git a/packages/react/src/Menu.tsx b/packages/react/src/Menu.tsx index e3a60520..25586857 100644 --- a/packages/react/src/Menu.tsx +++ b/packages/react/src/Menu.tsx @@ -52,21 +52,9 @@ function MenuEntry( data-theme={props.theme} href={props.href} target={props.target} - onClick={() => { - switch (props.action) { - case 'chat': { - openMarkprompt('chat'); - break; - } - case 'ticket': { - openMarkprompt('ticket'); - break; - } - case 'search': { - openMarkprompt('search'); - break; - } - } + onClick={async () => { + if (!props.action) return; + await openMarkprompt(props.action); }} > {props.iconId && ( diff --git a/packages/react/src/TicketDeflectionForm.tsx b/packages/react/src/TicketDeflectionForm.tsx index 1ce3becb..6e41fee3 100644 --- a/packages/react/src/TicketDeflectionForm.tsx +++ b/packages/react/src/TicketDeflectionForm.tsx @@ -8,7 +8,9 @@ import { } from 'react'; import { ChatView } from './chat/ChatView.js'; +import { ChatProvider } from './chat/provider.js'; import { useChatStore } from './chat/store.js'; +import { DEFAULT_MARKPROMPT_OPTIONS } from './constants.js'; import { GlobalStoreProvider } from './context/global/provider.js'; import { useGlobalStore, type GlobalOptions } from './context/global/store.js'; import { @@ -16,12 +18,7 @@ import { CustomCaseFormRenderer, } from './CreateTicketView.js'; import { ChevronLeftIcon, LoadingIcon } from './icons.js'; -import { - ChatProvider, - DEFAULT_MARKPROMPT_OPTIONS, - type MarkpromptOptions, - type TicketDeflectionFormView, -} from './index.js'; +import type { MarkpromptOptions, TicketDeflectionFormView } from './types.js'; import { NavigationMenu } from './ui/navigation-menu.js'; import { RichText } from './ui/rich-text.js'; import { useDefaults } from './useDefaults.js'; diff --git a/packages/react/src/Trigger.tsx b/packages/react/src/Trigger.tsx index c94e51e6..fcf135d7 100644 --- a/packages/react/src/Trigger.tsx +++ b/packages/react/src/Trigger.tsx @@ -53,8 +53,9 @@ export function Trigger(props: TriggerProps): JSX.Element { {children && (display !== 'plain' || hasMenu) && ( // todo: update element to button - // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ if (!onClick) return; diff --git a/packages/react/src/chat/AssistantMessage.tsx b/packages/react/src/chat/AssistantMessage.tsx index 25ad1b81..cb650365 100644 --- a/packages/react/src/chat/AssistantMessage.tsx +++ b/packages/react/src/chat/AssistantMessage.tsx @@ -54,8 +54,8 @@ export function AssistantMessage(props: AssistantMessageProps): JSX.Element { feedbackOptions, }); - const confirmToolCalls = (): void => { - submitToolCalls(message); + const confirmToolCalls = () => { + void submitToolCalls(message); }; const ToolCallConfirmation = useMemo( @@ -127,8 +127,8 @@ export function AssistantMessage(props: AssistantMessageProps): JSX.Element { variant="icons" data-show-feedback-always={showFeedbackAlways} className="MarkpromptPromptFeedback" - submitFeedback={(feedback, messageId) => { - submitFeedback(feedback, messageId); + submitFeedback={async (feedback, messageId) => { + await submitFeedback(feedback, messageId); feedbackOptions.onFeedbackSubmit?.( feedback, messages, diff --git a/packages/react/src/chat/ChatView.test.tsx b/packages/react/src/chat/ChatView.test.tsx index b88c9e00..40b54570 100644 --- a/packages/react/src/chat/ChatView.test.tsx +++ b/packages/react/src/chat/ChatView.test.tsx @@ -71,7 +71,7 @@ const ChatViewWithProvider = ({ }; const server = setupServer( - http.post(`${DEFAULT_OPTIONS.apiUrl!}/chat`, async () => { + http.post(`${DEFAULT_OPTIONS.apiUrl}/chat`, async () => { if (status >= 400) { return HttpResponse.json( { error: 'Internal server error' }, @@ -113,7 +113,7 @@ const server = setupServer( }, }); }), - http.post(DEFAULT_OPTIONS.apiUrl!, () => { + http.post(DEFAULT_OPTIONS.apiUrl, () => { return HttpResponse.json({ status: 'ok' }, { status: 200 }); }), ); @@ -156,7 +156,7 @@ describe('ChatView', () => { it('submits a chat request', async () => { response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -169,7 +169,7 @@ describe('ChatView', () => { }); it('allows selecting an example prompt', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); response = [{ content: 'answer' }]; @@ -199,7 +199,7 @@ describe('ChatView', () => { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -230,7 +230,7 @@ describe('ChatView', () => { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -262,7 +262,7 @@ describe('ChatView', () => { return Promise.resolve('test function result'); } - const user = await userEvent.setup(); + const user = userEvent.setup(); response = [ { tool_call: { name: 'do_a_thing', parameters: '{}' }, content: null }, @@ -320,7 +320,7 @@ describe('ChatView', () => { return Promise.resolve('test function result'); } - const user = await userEvent.setup(); + const user = userEvent.setup(); response = [ { tool_call: { name: 'do_a_thing', parameters: '{}' }, content: null }, @@ -385,7 +385,7 @@ describe('ChatView', () => { return Promise.resolve('test function result'); } - const user = await userEvent.setup(); + const user = userEvent.setup(); response = [ { tool_call: { name: 'do_a_thing', parameters: '{}' }, content: null }, @@ -434,7 +434,7 @@ describe('ChatView', () => { throw new Error('tool call failed'); } - const user = await userEvent.setup(); + const user = userEvent.setup(); response = [ { tool_call: { name: 'do_a_thing', parameters: '{}' }, content: null }, @@ -510,7 +510,7 @@ describe('ChatView', () => { ]; markpromptData = { references }; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -532,7 +532,7 @@ describe('ChatView', () => { ]; markpromptData = { references }; - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { ]; markpromptData = { references }; - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { ]; wait = true; - const user = await userEvent.setup(); + const user = userEvent.setup(); const { rerender } = render( { ]; wait = true; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -665,7 +665,7 @@ describe('ChatView', () => { it('aborts a pending chat request when an error is returned from the API', async () => { status = 500; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -687,7 +687,7 @@ describe('ChatView', () => { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -713,7 +713,7 @@ describe('ChatView', () => { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); @@ -742,7 +742,7 @@ describe('ChatView', () => { render(); - const user = await userEvent.setup(); + const user = userEvent.setup(); await user.type(screen.getByRole('textbox'), 'test'); await user.keyboard('{Enter}'); @@ -753,11 +753,10 @@ describe('ChatView', () => { expect(localStorage.getItem('markprompt')).not.toBeNull(); - expect( - JSON.parse(localStorage.getItem('markprompt')!).state.messagesByThreadId[ - threadId - ].messages[1].error, - ).toEqual({ + const error: unknown = JSON.parse(localStorage.getItem('markprompt')!).state + .messagesByThreadId[threadId].messages[1].error; + + expect(error).toEqual({ type: 'error', name: 'Error', message: 'Malformed response from Markprompt API', @@ -771,7 +770,7 @@ describe('ChatView', () => { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { markpromptData = { threadId, messageId }; response = [{ content: 'answer' }]; - const user = await userEvent.setup(); + const user = userEvent.setup(); render( { ]; wait = true; - const user = await userEvent.setup(); + const user = userEvent.setup(); render(); diff --git a/packages/react/src/chat/ChatViewForm.tsx b/packages/react/src/chat/ChatViewForm.tsx index 2f2eb421..9567e1fc 100644 --- a/packages/react/src/chat/ChatViewForm.tsx +++ b/packages/react/src/chat/ChatViewForm.tsx @@ -123,8 +123,6 @@ export function ChatViewForm(props: ChatViewFormProps): JSX.Element { ref={textAreaRef} className="MarkpromptPrompt" name="markprompt-prompt" - type="text" - // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus placeholder={chatOptions?.placeholder} labelClassName="MarkpromptPromptLabel" diff --git a/packages/react/src/chat/Messages.tsx b/packages/react/src/chat/Messages.tsx index c9b08344..916b71e1 100644 --- a/packages/react/src/chat/Messages.tsx +++ b/packages/react/src/chat/Messages.tsx @@ -48,14 +48,14 @@ export function CreateTicketButton({ } if (!messages || messages.length === 0 || !threadId) { - openMarkprompt('ticket'); + await openMarkprompt('ticket'); return; } setIsCreatingTicketSummary(true); await createTicketSummary?.(threadId, messages); setIsCreatingTicketSummary(false); - openMarkprompt('ticket', { + await openMarkprompt('ticket', { ticketDeflectionFormOptions: { defaultView: 'ticket', showBackLink: false, diff --git a/packages/react/src/chat/store.tsx b/packages/react/src/chat/store.tsx index 7b6dddce..1f4a5036 100644 --- a/packages/react/src/chat/store.tsx +++ b/packages/react/src/chat/store.tsx @@ -9,7 +9,8 @@ import { } from '@markprompt/core/chat'; import { isAbortError } from '@markprompt/core/utils'; import { createContext, useContext, type JSX } from 'react'; -import { createStore, useStore } from 'zustand'; +// eslint-disable-next-line import-x/no-deprecated +import { createStore, useStore, type StoreApi } from 'zustand'; import { createJSONStorage, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; @@ -252,7 +253,7 @@ export const createChatStore = ({ storeKey, apiUrl, headers, -}: CreateChatOptions) => { +}: CreateChatOptions): StoreApi => { if (!projectKey) { throw new Error( 'Markprompt: a project key is required. Make sure to pass your Markprompt project key to createChatStore.', @@ -548,7 +549,7 @@ export const createChatStore = ({ return tool?.requireConfirmation === false; }) ) { - get().submitToolCalls(responseMessage); + await get().submitToolCalls(responseMessage); } }, async submitToolCalls(message: ChatViewMessage) { @@ -665,7 +666,7 @@ export const createChatStore = ({ ['message', value.message], ['cause', value.cause], ].filter(([, v]) => v !== undefined), - ); + ) as { [key: string]: string }; } return value; }, @@ -744,6 +745,7 @@ export const ChatContext = createContext(null); export function useChatStore(selector: (state: ChatStoreState) => T): T { const store = useContext(ChatContext); if (!store) throw new Error('Missing ChatContext.Provider in the tree'); + // eslint-disable-next-line import-x/no-deprecated return useStore(store, selector); } diff --git a/packages/react/src/constants.test.ts b/packages/react/src/constants.test.ts index bfc06b10..d029c7b3 100644 --- a/packages/react/src/constants.test.ts +++ b/packages/react/src/constants.test.ts @@ -95,80 +95,80 @@ const algoliaSearchHits = [ describe('constants', () => { test('default references.getHref', () => { - expect(DEFAULT_MARKPROMPT_OPTIONS.references?.getHref?.(results[0]!)).toBe( + expect(DEFAULT_MARKPROMPT_OPTIONS.references?.getHref?.(results[0])).toBe( `${basePath}#${headingSlug}`, ); expect( - DEFAULT_MARKPROMPT_OPTIONS.references?.getHref?.(results[1]!), + DEFAULT_MARKPROMPT_OPTIONS.references?.getHref?.(results[1]), ).toEqual(basePath); }); test('default references.getLabel', () => { expect( - DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[0]!), + DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[0]), ).toEqual(heading); - expect(DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[1]!)).toBe( + expect(DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[1])).toBe( 'Home', ); expect( - DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[2]!), + DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[2]), ).toEqual(noTitleFileName); expect( - DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[3]!), + DEFAULT_MARKPROMPT_OPTIONS.references?.getLabel?.(results[3]), ).toEqual(noTitleFileName); }); test('default search.getHref', () => { - expect(DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(results[0]!)).toBe( + expect(DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(results[0])).toBe( `${basePath}#${headingSlug}`, ); - expect(DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(results[6]!)).toBe( + expect(DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(results[6])).toBe( `${urlPath}#${headingId}`, ); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(algoliaSearchHits[0]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getHref?.(algoliaSearchHits[0]), ).toEqual(algoliaSearchHits[0]?.url); }); test('default search.getHeading', () => { + expect(DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(results[0])).toEqual( + results[0]?.file.title, + ); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(results[0]!), - ).toEqual(results[0]?.file.title); - expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(results[1]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(results[1]), ).toBeUndefined(); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(algoliaSearchHits[0]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(algoliaSearchHits[0]), ).toBeUndefined(); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(algoliaSearchHits[1]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getHeading?.(algoliaSearchHits[1]), ).toEqual(algoliaSearchHits[1]?.hierarchy.lvl0); }); test('default search.getTitle', () => { expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[0]!, ''), + DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[0], ''), ).toEqual(results[0]?.meta?.leadHeading?.value); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[1]!, ''), + DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[1], ''), ).toEqual(results[1]?.file.title); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[4]!, 'aute'), + DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[4], 'aute'), ).toEqual(loremIpsumKwicSnippet); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[5]!, 'Some'), + DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(results[5], 'Some'), ).toEqual(shortContent); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(algoliaSearchHits[0]!, ''), + DEFAULT_MARKPROMPT_OPTIONS.search?.getTitle?.(algoliaSearchHits[0], ''), ).toEqual(algoliaSearchHits[0]?.hierarchy.lvl1); }); test('default search.getSubtitle', () => { expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getSubtitle?.(results[0]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getSubtitle?.(results[0]), ).toBeUndefined(); expect( - DEFAULT_MARKPROMPT_OPTIONS.search?.getSubtitle?.(algoliaSearchHits[0]!), + DEFAULT_MARKPROMPT_OPTIONS.search?.getSubtitle?.(algoliaSearchHits[0]), ).toEqual(algoliaSearchHits[0]?.hierarchy.lvl2); }); }); diff --git a/packages/react/src/context/global/store.ts b/packages/react/src/context/global/store.ts index 9684958a..d75a95fd 100644 --- a/packages/react/src/context/global/store.ts +++ b/packages/react/src/context/global/store.ts @@ -2,9 +2,10 @@ import { submitChat, type ChatCompletionMessageParam, } from '@markprompt/core/chat'; -import { isAbortError } from '@markprompt/core/utils'; +import { isAbortError, getMessageTextContent } from '@markprompt/core/utils'; import { createContext, useContext } from 'react'; import { createStore, type StoreApi } from 'zustand'; +// eslint-disable-next-line import-x/no-deprecated import { useStore } from 'zustand'; import { immer } from 'zustand/middleware/immer'; @@ -12,6 +13,7 @@ import { getInitialView } from './utils.js'; import type { ChatViewMessage } from '../../chat/store.js'; import { toValidApiMessages } from '../../chat/utils.js'; import type { MarkpromptOptions, View } from '../../types.js'; +import { isPresent } from '../../utils.js'; export type GlobalOptions = MarkpromptOptions & { projectKey: string }; @@ -26,7 +28,7 @@ interface State { createTicketSummary: ( threadId: string, messages: ChatViewMessage[], - ) => void; + ) => Promise; }; } @@ -107,8 +109,11 @@ Output: const conversation = toValidApiMessages(messages) .map((m) => { - return `${m.role === 'user' ? 'User' : 'Assistant'}:\n\n${m.content}`; + const content = getMessageTextContent(m); + if (!content) return; + return `${m.role === 'user' ? 'User' : 'AI'}: ${content}`; }) + .filter(isPresent) .join('\n\n==============================\n\n'); const apiMessages = [ @@ -176,5 +181,6 @@ export function useGlobalStore(selector: (state: State) => T): T { 'Missing GlobalStoreProvider. Make sure to wrap your component tree with .', ); } + // eslint-disable-next-line import-x/no-deprecated return useStore(store, selector); } diff --git a/packages/react/src/feedback/Feedback.tsx b/packages/react/src/feedback/Feedback.tsx index d27aac38..078c14ca 100644 --- a/packages/react/src/feedback/Feedback.tsx +++ b/packages/react/src/feedback/Feedback.tsx @@ -43,9 +43,9 @@ export function Feedback(props: FeedbackProps): JSX.Element { const [feedback, setFeedback] = useState(); - function handleFeedback(feedback: PromptFeedback): void { - submitFeedback(feedback, messageId); + async function handleFeedback(feedback: PromptFeedback): Promise { setFeedback(feedback); + await submitFeedback(feedback, messageId); } useEffect(() => { diff --git a/packages/react/src/feedback/csat-picker.tsx b/packages/react/src/feedback/csat-picker.tsx index b0ce18d0..a7213d8b 100644 --- a/packages/react/src/feedback/csat-picker.tsx +++ b/packages/react/src/feedback/csat-picker.tsx @@ -1,3 +1,5 @@ +/** eslint-disable @typescript-eslint/no-misused-promises */ +/** eslint-disable @typescript-eslint/no-misused-promises */ import { DEFAULT_OPTIONS } from '@markprompt/core/constants'; import type { CSAT } from '@markprompt/core/feedback'; import { @@ -43,7 +45,7 @@ export function CSATReasonTextArea({ heading, thankYou, }: { - onSubmit: (reason: string) => void; + onSubmit: (reason: string) => Promise; heading: string | undefined; thankYou: string | undefined; }): JSX.Element { @@ -107,8 +109,6 @@ export function CSATReasonTextArea({ ref={textAreaRef} className="MarkpromptPrompt" name="markprompt-csat-reason" - type="text" - // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus labelClassName="MarkpromptPromptLabel" textAreaContainerClassName="MarkpromptTextAreaContainer" @@ -143,10 +143,10 @@ export function CSATPicker(props: CSATPickerProps): JSX.Element { }); const submitCSAT = useCallback( - (value: CSAT) => { + async (value: CSAT) => { setTempValue(value); setPermanentValue(value); - submitThreadCSAT(threadId, value); + await submitThreadCSAT(threadId, value); }, [submitThreadCSAT, threadId], ); @@ -165,6 +165,7 @@ export function CSATPicker(props: CSATPickerProps): JSX.Element { ? getHeading(tempValue) || feedbackOptions.headingCSAT : feedbackOptions.headingCSAT}

+ {/* biome-ignore lint/nursery/noStaticElementInteractions: only used for a highlight style */}
{ setIsHovering(true); @@ -183,7 +184,7 @@ export function CSATPicker(props: CSATPickerProps): JSX.Element { setTempValue((i + 1) as CSAT); }} onClick={() => { - submitCSAT((i + 1) as CSAT); + void submitCSAT((i + 1) as CSAT); }} key={`star-${_}`} className="MarkpromptMessageCSATStar" diff --git a/packages/react/src/feedback/useFeedback.ts b/packages/react/src/feedback/useFeedback.ts index 738d34f3..e56d9646 100644 --- a/packages/react/src/feedback/useFeedback.ts +++ b/packages/react/src/feedback/useFeedback.ts @@ -23,11 +23,14 @@ export interface UseFeedbackResult { /** Abort any pending feedback submission */ abort: () => void; /** Submit feedback for the current message */ - submitFeedback: (feedback: PromptFeedback, messageId?: string) => void; + submitFeedback: ( + feedback: PromptFeedback, + messageId?: string, + ) => Promise; /** Submit CSAT for a thread */ - submitThreadCSAT: (threadId: string, csat: CSAT) => void; + submitThreadCSAT: (threadId: string, csat: CSAT) => Promise; /** Submit CSAT reason for a thread */ - submitThreadCSATReason: (threadId: string, reason: string) => void; + submitThreadCSATReason: (threadId: string, reason: string) => Promise; } export function useFeedback({ diff --git a/packages/react/src/primitives/headless.test.tsx b/packages/react/src/primitives/headless.test.tsx index c6bb91fa..66a78ee8 100644 --- a/packages/react/src/primitives/headless.test.tsx +++ b/packages/react/src/primitives/headless.test.tsx @@ -19,7 +19,7 @@ import * as Markprompt from './headless.js'; let searchResults: SearchResult[] = []; let status = 200; const server = setupServer( - http.get(DEFAULT_OPTIONS.apiUrl!, () => { + http.get(DEFAULT_OPTIONS.apiUrl, () => { return HttpResponse.json( { data: searchResults }, { @@ -75,7 +75,7 @@ test('Returns children when display is plain', () => { Search - + Caret @@ -209,7 +209,7 @@ test('Prompt changes trigger user-defined callbacks', async () => { render( - + , ); @@ -225,11 +225,11 @@ test('Prompt changes updates prompt state', async () => { render( - + , ); - const input = await screen.findByRole('textbox'); + const input = await screen.findByRole('searchbox'); await user.type(input, 'test'); expect(input).toHaveValue('test'); diff --git a/packages/react/src/primitives/headless.tsx b/packages/react/src/primitives/headless.tsx index f45ba125..3427cdb3 100644 --- a/packages/react/src/primitives/headless.tsx +++ b/packages/react/src/primitives/headless.tsx @@ -2,13 +2,13 @@ import type { FileSectionReference } from '@markprompt/core/types'; import * as Dialog from '@radix-ui/react-dialog'; import { forwardRef, + isValidElement, memo, useCallback, useEffect, useRef, useState, - type ComponentPropsWithRef, - type ComponentPropsWithoutRef, + type ComponentProps, type ComponentType, type ElementType, type FormEventHandler, @@ -32,7 +32,10 @@ import type { SearchResultComponentProps, } from '../types.js'; -type RootProps = Dialog.DialogProps & { display?: MarkpromptDisplay }; +type RootProps = Omit & { + display?: MarkpromptDisplay; + onOpenChange?: (this: void, open: boolean) => void; +}; /** * The Markprompt context provider and dialog root. @@ -72,7 +75,7 @@ function DialogRootWithAbort(props: Dialog.DialogProps): JSX.Element { ); } -type DialogTriggerProps = ComponentPropsWithRef; +type DialogTriggerProps = ComponentProps; /** * A button to open the Markprompt dialog. */ @@ -83,7 +86,7 @@ const DialogTrigger = forwardRef( ); DialogTrigger.displayName = 'Markprompt.DialogTrigger'; -type PortalProps = ComponentPropsWithoutRef; +type PortalProps = ComponentProps; /** * The Markprompt dialog portal. */ @@ -92,7 +95,7 @@ function Portal(props: PortalProps): JSX.Element { } Portal.displayName = 'Markprompt.Portal'; -type OverlayProps = ComponentPropsWithRef; +type OverlayProps = ComponentProps; /** * The Markprompt dialog overlay. */ @@ -101,7 +104,7 @@ const Overlay = forwardRef((props, ref) => { }); Overlay.displayName = 'Markprompt.Overlay'; -type ContentProps = ComponentPropsWithoutRef; +type ContentProps = ComponentProps; /** * The Markprompt dialog content. @@ -124,7 +127,7 @@ export interface BrandingProps { branding?: { show?: boolean; type?: 'plain' | 'text' }; } -type PlainContentProps = ComponentPropsWithRef<'div'>; +type PlainContentProps = ComponentProps<'div'>; /** * The Markprompt plain content. */ @@ -139,7 +142,7 @@ const PlainContent = forwardRef( ); PlainContent.displayName = 'Markprompt.PlainContent'; -type CloseProps = ComponentPropsWithRef; +type CloseProps = ComponentProps; /** * A button to close the Markprompt dialog and abort an ongoing request. */ @@ -150,7 +153,7 @@ const Close = forwardRef( ); Close.displayName = 'Markprompt.Close'; -type TitleProps = ComponentPropsWithRef & { +type TitleProps = ComponentProps & { hide?: boolean; }; const Title = forwardRef((props, ref) => { @@ -163,7 +166,7 @@ const Title = forwardRef((props, ref) => { }); Title.displayName = 'Markprompt.Title'; -type DescriptionProps = ComponentPropsWithRef & { +type DescriptionProps = ComponentProps & { hide?: boolean; }; /** @@ -181,7 +184,7 @@ const Description = forwardRef( ); Description.displayName = 'Markprompt.Description'; -type FormProps = ComponentPropsWithRef<'form'>; +type FormProps = ComponentProps<'form'>; /** * A form which, when submitted, submits the current prompt. */ @@ -189,7 +192,7 @@ const Form = forwardRef(function Form(props, ref) { return ; }); -interface PromptInnerProps { +interface PromptBaseProps { /** The label for the input. */ label?: ReactNode; /** The class name of the label element. */ @@ -214,7 +217,7 @@ interface PromptInnerProps { submitOnEnter?: boolean; } -type PromptProps = ComponentPropsWithRef<'input'> & PromptInnerProps; +type PromptProps = ComponentProps<'textarea'> & PromptBaseProps; /** * The Markprompt input prompt. User input will update the prompt in the Markprompt context. @@ -233,7 +236,6 @@ const Prompt = forwardRef( sendButtonClassName, placeholder, spellCheck = false, - type = 'search', showSubmitButton = true, isLoading, Icon, @@ -244,14 +246,10 @@ const Prompt = forwardRef( onSubmit, onKeyDown, ...rest - } = props as any; + } = props; const handleKeyDown = useCallback( (event: KeyboardEvent): void => { - if (type === 'search') { - onKeyDown?.(event); - return; - } if (event.key === 'Enter' && !event.shiftKey) { if (submitOnEnter !== false) { event.preventDefault(); @@ -262,11 +260,9 @@ const Prompt = forwardRef( } } }, - [onKeyDown, onSubmit, submitOnEnter, type], + [onSubmit, submitOnEnter], ); - const Comp = type === 'search' ? 'input' : TextareaAutoSize; - return ( <> {label && ( @@ -275,23 +271,21 @@ const Prompt = forwardRef( )}
-
@@ -315,6 +309,106 @@ const Prompt = forwardRef( ); Prompt.displayName = 'Markprompt.Prompt'; +interface SearchPromptBaseProps { + /** The label for the input. */ + label?: ReactNode; + /** The class name of the label element. */ + labelClassName?: string; + /** The class name of the input container. */ + containerClassName?: string; + /** The class name of the send button element. */ + sendButtonClassName?: string; + /** The label for the submit button. */ + buttonLabel?: string; + /** Show an icon next to the send button. */ + showSubmitButton?: boolean; + /** If the answer is loading. */ + isLoading?: boolean; + /** Icon for the button. */ + Icon?: ReactNode; + /** + * Prompt type + * @defaults search + */ + type?: 'search' | 'input'; +} + +type SearchPromptProps = ComponentProps<'input'> & SearchPromptBaseProps; + +/** + * The Markprompt input prompt. User input will update the prompt in the Markprompt context. + */ +const SearchPrompt = forwardRef( + function Prompt(props, ref) { + const { + autoCapitalize = 'none', + autoComplete = 'off', + autoCorrect = 'off', + autoFocus = true, + label, + buttonLabel = 'Send', + labelClassName, + sendButtonClassName, + containerClassName, + placeholder, + spellCheck = false, + type = 'search', + showSubmitButton = true, + isLoading, + Icon, + name, + className, + onKeyDown, + ...rest + } = props; + + return ( + <> + {label && ( + + )} +
+ +
+ {showSubmitButton && ( + + )} + + ); + }, +); +Prompt.displayName = 'Markprompt.Prompt'; + // between the type that react-markdown exposes, and what is actually // serves. interface CopyContentButtonProps { @@ -327,11 +421,18 @@ function CopyContentButton(props: CopyContentButtonProps): JSX.Element { const [didJustCopy, setDidJustCopy] = useState(false); const handleClick = (): void => { - navigator.clipboard.writeText(content); - setDidJustCopy(true); - setTimeout(() => { - setDidJustCopy(false); - }, 2000); + navigator.clipboard + .writeText(content) + .then(() => { + setDidJustCopy(true); + const id = setTimeout(() => { + setDidJustCopy(false); + }, 2000); + return id; + }) + .catch((error) => { + console.error(error); + }); }; return ( @@ -367,10 +468,17 @@ function CopyContentButton(props: CopyContentButtonProps): JSX.Element { } CopyContentButton.displayName = 'Markprompt.CopyContentButton'; -type HighlightedCodeProps = React.ClassAttributes & - React.HTMLAttributes & { - state?: ChatLoadingState; - }; +type HighlightedCodeProps = ComponentProps<'pre'> & { + state?: ChatLoadingState; +}; + +declare global { + interface hljs { + highlightAll: () => void; + } + + var hljs: hljs | undefined; +} function HighlightedCode(props: HighlightedCodeProps): JSX.Element { const { children, className, state, ...rest } = props; @@ -382,8 +490,12 @@ function HighlightedCode(props: HighlightedCodeProps): JSX.Element { // we can syntax highlight. This trick allows us to provide // syntax highlighting without imposing a large extra // package as part of the markprompt-js bundle. - - ((globalThis as any).hljs as any)?.highlightAll(); + if ( + globalThis.hljs && + typeof globalThis.hljs.highlightAll === 'function' + ) { + globalThis.hljs.highlightAll(); + } } }, [children, state]); @@ -394,10 +506,7 @@ function HighlightedCode(props: HighlightedCodeProps): JSX.Element { ); } -type AnswerProps = Omit< - ComponentPropsWithoutRef, - 'children' -> & { +type AnswerProps = Omit, 'children'> & { answer: string; state?: ChatLoadingState; copyButtonClassName?: string; @@ -423,10 +532,25 @@ function Answer(props: AnswerProps): JSX.Element { {...rest} remarkPlugins={remarkPlugins} components={{ - a: (props) => , - pre: (props) => { + a: (props: ComponentProps<'a'>) => , + pre: (props: ComponentProps<'pre'>) => { const { children, className, ...rest } = props; + let content = ''; + + if ( + children && + typeof children === 'object' && + 'props' in children && + typeof children.props === 'object' && + children.props !== null && + 'children' in children.props && + isValidElement(children.props.children) && + typeof children.props.children === 'string' + ) { + content = children.props.children; + } + return (
@@ -495,8 +613,7 @@ interface AutoScrollerInnerProps { discreteScrollTrigger?: number; } -type AutoScrollerProps = ComponentPropsWithoutRef<'div'> & - AutoScrollerInnerProps; +type AutoScrollerProps = ComponentProps<'div'> & AutoScrollerInnerProps; /** * A component that automatically scrolls to the bottom. @@ -767,6 +884,7 @@ export { Prompt, ForwardedReferences as References, Root, + SearchPrompt, SearchResult, SearchResults, Title, @@ -783,6 +901,7 @@ export { type PromptProps, type ReferencesProps, type RootProps, + type SearchPromptProps, type SearchResultProps, type SearchResultsProps, type TitleProps, diff --git a/packages/react/src/search/SearchBoxTrigger.tsx b/packages/react/src/search/SearchBoxTrigger.tsx index a9e9eaf1..8bfa529a 100644 --- a/packages/react/src/search/SearchBoxTrigger.tsx +++ b/packages/react/src/search/SearchBoxTrigger.tsx @@ -55,7 +55,7 @@ export function SearchBoxTrigger(props: SearchBoxTriggerProps): JSX.Element { - {navigator.platform.indexOf('Mac') === 0 || + {navigator.platform.startsWith('Mac') || navigator.platform === 'iPhone' ? ( diff --git a/packages/react/src/search/SearchView.test.tsx b/packages/react/src/search/SearchView.test.tsx index 6b7e5b4a..646ff42f 100644 --- a/packages/react/src/search/SearchView.test.tsx +++ b/packages/react/src/search/SearchView.test.tsx @@ -25,7 +25,7 @@ let results: SearchResult[] | AlgoliaDocSearchHit[] = []; let debug: unknown; const server = setupServer( - http.get(`${DEFAULT_OPTIONS.apiUrl!}/search`, () => { + http.get(`${DEFAULT_OPTIONS.apiUrl}/search`, () => { if (status >= 400) { return HttpResponse.json( { error: 'Server error', debug }, @@ -77,7 +77,7 @@ describe('SearchView', () => { it('displays search queries', async () => { const query = 'test query'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { @@ -155,7 +155,7 @@ describe('SearchView', () => { it('display an empty state when there are no search results', async () => { const query = 'testquery'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = []; @@ -173,7 +173,7 @@ describe('SearchView', () => { 'allows users to select search queries', async () => { const query = 'test'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { @@ -215,23 +215,25 @@ describe('SearchView', () => { }); // first item selected by default - await expect( - screen.getByRole('option', { selected: true }), - ).toHaveAttribute('id', 'markprompt-result-0'); + expect(screen.getByRole('option', { selected: true })).toHaveAttribute( + 'id', + 'markprompt-result-0', + ); // select item on arrow down await user.keyboard('{ArrowDown}'); - await expect( - screen.getByRole('option', { selected: true }), - ).toHaveAttribute('id', 'markprompt-result-1'); + expect(screen.getByRole('option', { selected: true })).toHaveAttribute( + 'id', + 'markprompt-result-1', + ); // select item on mousemove // From Michael: This test currently fails - it doesn't trigger // the mouse move event. // await userEvent.hover(screen.getByRole('link', { name: 'result 2' })); - // await expect( + // expect( // screen.getByRole('option', { selected: true }), // ).toHaveAttribute('id', 'markprompt-result-2'); @@ -239,25 +241,27 @@ describe('SearchView', () => { await user.keyboard('{ArrowUp}'); await user.keyboard('{ArrowDown}'); - await expect( - screen.getByRole('option', { selected: true }), - ).toHaveAttribute('id', 'markprompt-result-1'); + expect(screen.getByRole('option', { selected: true })).toHaveAttribute( + 'id', + 'markprompt-result-1', + ); // don't go past the last result await user.keyboard('{ArrowDown}'); await user.keyboard('{ArrowDown}'); await user.keyboard('{ArrowDown}'); - await expect( - screen.getByRole('option', { selected: true }), - ).toHaveAttribute('id', 'markprompt-result-2'); + expect(screen.getByRole('option', { selected: true })).toHaveAttribute( + 'id', + 'markprompt-result-2', + ); }, { retry: 3 }, ); it('reselects the first search result when the search query changes', async () => { const query = 'test query'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { @@ -292,7 +296,7 @@ describe('SearchView', () => { it.skip('allows users to open search results', async () => { const query = 'test query'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { @@ -314,12 +318,12 @@ describe('SearchView', () => { await user.keyboard('{Enter}'); - await expect(window.location.href).toContain('#file'); + expect(window.location.href).toContain('#file'); }); it('highlights matches', async () => { const query = 'test'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { @@ -348,7 +352,7 @@ describe('SearchView', () => { }); it('can use algolia as a search provider', async () => { - const user = await userEvent.setup(); + const user = userEvent.setup(); const query = 'react'; results = [ @@ -406,7 +410,7 @@ describe('SearchView', () => { it('logs debug information', async () => { const query = 'test query'; - const user = await userEvent.setup(); + const user = userEvent.setup(); results = [ { diff --git a/packages/react/src/search/SearchView.tsx b/packages/react/src/search/SearchView.tsx index ca500399..c5512abf 100644 --- a/packages/react/src/search/SearchView.tsx +++ b/packages/react/src/search/SearchView.tsx @@ -100,8 +100,8 @@ export function SearchView(props: SearchViewProps): JSX.Element { DEFAULT_MARKPROMPT_OPTIONS.search, ); - const formRef = useRef(null); - const textAreaRef = useRef(null); + const formRef = useRef(null); + const searchInputRef = useRef(null); const { abort, @@ -153,7 +153,7 @@ export function SearchView(props: SearchViewProps): JSX.Element { // biome-ignore lint/correctness/useExhaustiveDependencies: we want to run the effect when activeView changes useEffect(() => { // Bring form input in focus when activeView changes. - textAreaRef.current?.focus(); + searchInputRef.current?.focus(); }, [activeView]); useEffect(() => { @@ -264,9 +264,9 @@ export function SearchView(props: SearchViewProps): JSX.Element { ); const handleChange: ChangeEventHandler = useCallback( - (event) => { + async (event) => { setSearchQuery(event.target.value); - submitSearchQuery(event.target.value); + await submitSearchQuery(event.target.value); }, [setSearchQuery, submitSearchQuery], ); @@ -287,13 +287,13 @@ export function SearchView(props: SearchViewProps): JSX.Element { onSubmit={handleSubmit} >
- {isAskVisible && ( - // todo: should this use a different (interactive) element? - // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ diff --git a/packages/react/src/search/useSearch.test.ts b/packages/react/src/search/useSearch.test.ts index 571a65d0..4a8ae94c 100644 --- a/packages/react/src/search/useSearch.test.ts +++ b/packages/react/src/search/useSearch.test.ts @@ -23,7 +23,7 @@ let searchResults: SearchResult[] | AlgoliaDocSearchHit[] = []; let status = 200; const server = setupServer( - http.get(`${DEFAULT_OPTIONS.apiUrl!}/search`, () => { + http.get(`${DEFAULT_OPTIONS.apiUrl}/search`, () => { return HttpResponse.json( { data: searchResults }, { @@ -117,20 +117,19 @@ describe('useSearch', () => { ); await result.current.submitSearchQuery('react'); - await waitFor(() => expect(result.current.searchResults.length).toBe(3)); expect(result.current.searchResults[0]?.href).toBe( - (searchResults as SearchResult[])[0]?.file.path, + searchResults[0]?.file.path, ); expect(result.current.searchResults[0]?.title).toBe( - (searchResults as SearchResult[])[0]?.file.title, + searchResults[0]?.file.title, ); expect(result.current.searchResults[1]?.title).toBe( - (searchResults as SearchResult[])[1]?.meta?.leadHeading?.value, + searchResults[1]?.meta?.leadHeading?.value, ); expect(result.current.searchResults[2]?.title).toBe( - (searchResults as SearchResult[])[2]?.snippet, + searchResults[2]?.snippet, ); }); diff --git a/packages/react/src/search/useSearch.ts b/packages/react/src/search/useSearch.ts index 8f0233f1..6e41d1b8 100644 --- a/packages/react/src/search/useSearch.ts +++ b/packages/react/src/search/useSearch.ts @@ -33,7 +33,7 @@ export interface UseSearchResult { state: SearchLoadingState; abort: () => void; setSearchQuery: (searchQuery: string) => void; - submitSearchQuery: (searchQuery: string) => void; + submitSearchQuery: (searchQuery: string) => Promise; } export function useSearch({ @@ -78,9 +78,7 @@ export function useSearch({ ...searchOptions, signal: controller.signal, }) as Promise - ).then((result) => result?.hits || []) as Promise< - AlgoliaDocSearchHit[] - >; + ).then((result) => result?.hits || []); } else { promise = ( submitSearchQueryToMarkprompt(searchQuery, projectKey, { diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index d9f9af1e..a9cc2b71 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -510,7 +510,7 @@ export interface TriggerOptions { */ floating?: boolean; /** Do you use a custom element as the dialog trigger? */ - customElement?: boolean | ReactNode; + customElement?: ReactNode; /** * Custom image icon source for the open button **/ @@ -760,7 +760,7 @@ export interface MarkpromptOptions { * Trigger component, such as a search button or a floating chat bubble. * @default undefined **/ - children?: React.ReactNode; + children?: ReactNode; /** * The way to display the chat/search content. * @default "sheet" diff --git a/packages/react/src/useDefaults.test.tsx b/packages/react/src/useDefaults.test.tsx index b041bd71..00ff7913 100644 --- a/packages/react/src/useDefaults.test.tsx +++ b/packages/react/src/useDefaults.test.tsx @@ -1,5 +1,10 @@ import { renderHook } from '@testing-library/react'; -import { cloneElement } from 'react'; +import { + cloneElement, + type Attributes, + type ReactElement, + type ReactNode, +} from 'react'; import { describe, expect, it, vi } from 'vitest'; import { useDefaults } from './useDefaults.js'; @@ -8,9 +13,15 @@ vi.mock('react', async (importOriginal) => { const mod = await importOriginal(); return { ...mod, - cloneElement: vi.fn((element, props, ...children) => { - return mod.cloneElement(element, props, ...children); - }), + cloneElement: vi.fn( + ( + element: ReactElement, + props: (Partial & Attributes) | undefined, + ...children: ReactNode[] + ) => { + return mod.cloneElement(element, props, ...children); + }, + ), }; }); diff --git a/packages/react/src/useDefaults.ts b/packages/react/src/useDefaults.ts index b4a60c45..ac27cf9c 100644 --- a/packages/react/src/useDefaults.ts +++ b/packages/react/src/useDefaults.ts @@ -20,6 +20,15 @@ type DeepMerge = T extends { [key: string]: unknown } : T : U; +// biome-ignore lint/complexity/noBannedTypes: we need it to match the parameter type of isValidElement +function isObjectOrNullish(value: unknown): value is {} | null | undefined { + return ( + value === null || + value === undefined || + (typeof value === 'object' && !Array.isArray(value)) + ); +} + // defaults only merges the first level of properties, we need to make sure that // deeper nested properties are merged as well. export function useDefaults< @@ -32,12 +41,12 @@ export function useDefaults< defaults( cloneDeepWith(options, (value) => { // don't clone React elements with lodash, they won't render properly after - if (isValidElement(value)) { + if (isObjectOrNullish(value) && isValidElement(value)) { return cloneElement(value); } }), defaultOptions, - ), + ) as U extends undefined ? T : DeepMerge, [defaultOptions, options], ); } diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index a9d3ed15..43540ba9 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -22,7 +22,16 @@ export function isIterable(obj: unknown): boolean { return false; } - return typeof (obj as any)[Symbol.iterator] === 'function'; + // Type guard to check if obj is an object first + if (typeof obj !== 'object') { + return false; + } + + // Now we can safely check for Symbol.iterator + return ( + typeof (obj as { [Symbol.iterator]?: unknown })[Symbol.iterator] === + 'function' + ); } /** @@ -129,14 +138,17 @@ export const emitter = new Emittery<{ * Open Markprompt programmatically. Useful for building a custom trigger * or opening the Markprompt dialog in response to other user actions. */ -export function openMarkprompt(view?: View, options?: ViewOptions): void { - emitter.emit('open', { view, options }); +export function openMarkprompt( + view?: View, + options?: ViewOptions, +): Promise { + return emitter.emit('open', { view, options }); } /** * Close Markprompt programmatically. Useful for building a custom trigger * or closing the Markprompt dialog in response to other user actions. */ -export function closeMarkprompt(): void { - emitter.emit('close'); +export function closeMarkprompt(): Promise { + return emitter.emit('close'); } diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 45308b25..6b061e9a 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -15,8 +15,8 @@ // transpilation "module": "nodenext", "moduleResolution": "nodenext", - "outDir": "dist/", "rootDir": "src/", + "outDir": "dist/", "sourceMap": true, "declaration": true, @@ -28,5 +28,5 @@ "lib": ["dom", "dom.iterable", "es2023"] }, "include": ["src/"], - "exclude": [".turbo/", "**/node_modules", "dist/", "src/**/*.test.ts"] + "exclude": [".turbo/", "**/node_modules", "dist/"] } diff --git a/packages/web/eslint.config.js b/packages/web/eslint.config.js index f1d71754..4446bf52 100644 --- a/packages/web/eslint.config.js +++ b/packages/web/eslint.config.js @@ -16,5 +16,4 @@ export default [ }, ...configs.react, ...configs.vitest, - ...configs.biome, ]; diff --git a/packages/web/package.json b/packages/web/package.json index aac5f015..8683eb27 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -25,6 +25,7 @@ "prepack": "node scripts/build.js" }, "dependencies": { + "@markprompt/core": "workspace:*", "@markprompt/react": "workspace:*", "lodash-es": "^4.17.21" }, diff --git a/packages/web/scripts/analyze.js b/packages/web/scripts/analyze.js index 9c674389..0b227d82 100644 --- a/packages/web/scripts/analyze.js +++ b/packages/web/scripts/analyze.js @@ -7,7 +7,7 @@ import { config } from './config.js'; const result = await esbuild.build({ ...config, minify: true, metafile: true }); // outputs a `meta.json` which can be uploaded to https://esbuild.github.io/analyze/ -fs.writeFile('meta.json', JSON.stringify(result.metafile)); +void fs.writeFile('meta.json', JSON.stringify(result.metafile)); // outputs an analysis to the console console.log(await esbuild.analyzeMetafile(result.metafile)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cf7aa1a..d797fd7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -625,14 +625,14 @@ importers: version: 4.5.5(@types/react@19.0.1)(immer@10.1.1)(react@19.0.0) devDependencies: '@testing-library/dom': - specifier: ^10.0.0 + specifier: ^10.4.0 version: 10.4.0 '@testing-library/jest-dom': - specifier: ^6.4.2 + specifier: ^6.6.3 version: 6.6.3 '@testing-library/react': - specifier: ^15.0.2 - version: 15.0.7(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^16.1.0 + version: 16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -663,6 +663,9 @@ importers: packages/web: dependencies: + '@markprompt/core': + specifier: workspace:* + version: link:../core '@markprompt/react': specifier: workspace:* version: link:../react @@ -3166,16 +3169,20 @@ packages: resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@15.0.7': - resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} + '@testing-library/react@16.1.0': + resolution: {integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==} engines: {node: '>=18'} peerDependencies: + '@testing-library/dom': ^10.0.0 '@types/react': ^19 + '@types/react-dom': ^19 react: ^19 react-dom: ^19 peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true '@testing-library/user-event@14.5.2': resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} @@ -12240,15 +12247,15 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@15.0.7(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@testing-library/react@16.1.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.26.0 '@testing-library/dom': 10.4.0 - '@types/react-dom': 19.0.2(@types/react@19.0.1) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: '@types/react': 19.0.1 + '@types/react-dom': 19.0.2(@types/react@19.0.1) '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: diff --git a/turbo.json b/turbo.json index 1094e7ab..d4de09ee 100644 --- a/turbo.json +++ b/turbo.json @@ -20,7 +20,6 @@ "dependsOn": ["^build", "@markprompt/eslint-config#build"] }, "//#lint:md": {}, - "//#lint:ts": {}, "lint:ts": { "dependsOn": ["^build"] }, diff --git a/vitest.workspace.json b/vitest.workspace.json deleted file mode 100644 index 6ad17000..00000000 --- a/vitest.workspace.json +++ /dev/null @@ -1 +0,0 @@ -["packages/*"] diff --git a/vitest.workspace.ts b/vitest.workspace.ts new file mode 100644 index 00000000..16735a87 --- /dev/null +++ b/vitest.workspace.ts @@ -0,0 +1,3 @@ +import { defineWorkspace } from 'vitest/config'; + +export default defineWorkspace(['packages/*']);