tldraw/apps/docs/app/api/ai/route.ts
Dan Groshev d7b80baa31
use native structuredClone on node, cloudflare workers, and in tests (#3166)
Currently, we only use native `structuredClone` in the browser, falling
back to `JSON.parse(JSON.stringify(...))` elsewhere, despite Node
supporting `structuredClone` [since
v17](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)
and Cloudflare Workers supporting it [since
2022](https://blog.cloudflare.com/standards-compliant-workers-api/).
This PR adjusts our shim to use the native `structuredClone` on all
platforms, if available.

Additionally, `jsdom` doesn't implement `structuredClone`, a bug [open
since 2022](https://github.com/jsdom/jsdom/issues/3363). This PR patches
`jsdom` environment in all packages/apps that use it for tests.

Also includes a driveby removal of `deepCopy`, a function that is
strictly inferior to `structuredClone`.

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [x] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [x] `improvement` — Improving existing features
- [x] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Test Plan

1. A smoke test would be enough

- [ ] Unit Tests
- [x] End to end tests
2024-03-18 17:16:09 +00:00

185 lines
5.1 KiB
TypeScript

import { SearchResult } from '@/types/search-types'
import { getDb } from '@/utils/ContentDatabase'
import { SEARCH_RESULTS, searchBucket, sectionTypeBucket } from '@/utils/search-api'
import { structuredClone } from '@tldraw/utils'
import assert from 'assert'
import { NextRequest } from 'next/server'
type Data = {
results: {
articles: SearchResult[]
apiDocs: SearchResult[]
examples: SearchResult[]
}
status: 'success' | 'error' | 'no-query'
}
const BANNED_HEADINGS = ['new', 'constructor', 'properties', 'example', 'methods']
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const query = searchParams.get('q')?.toLowerCase()
if (!query) {
return new Response(
JSON.stringify({
results: structuredClone(SEARCH_RESULTS),
status: 'error',
error: 'No query',
}),
{
status: 400,
}
)
}
try {
const results: Data['results'] = structuredClone(SEARCH_RESULTS)
const db = await getDb()
const getVectorDb = (await import('@/utils/ContentVectorDatabase')).getVectorDb
const vdb = await getVectorDb()
const queryResults = await vdb.query(query, 25)
queryResults.sort((a, b) => b.score - a.score)
const headings = (
await Promise.all(
queryResults.map(async (result) => {
try {
if (result.type !== 'heading') return // bleg
const article = await db.db.get(
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
result.id
)
assert(article, `No article found for heading ${result.id}`)
const category = await db.db.get(
`SELECT id, title FROM categories WHERE id = ?`,
article.categoryId
)
const section = await db.db.get(
`SELECT id, title FROM sections WHERE id = ?`,
article.sectionId
)
const heading = await db.db.get(`SELECT * FROM headings WHERE slug = ?`, result.slug)
assert(heading, `No heading found for ${result.id} ${result.slug}`)
return {
id: result.id,
article,
category,
section,
heading,
score: result.score,
}
} catch (e: any) {
console.error(e.message)
// something went wrong
return
}
})
)
).filter(Boolean)
const visited = new Set<string>()
for (const result of headings) {
if (!result) continue
if (visited.has(result.id)) continue
visited.add(result.id)
const { category, section, article, heading, score } = result
const isUncategorized = category.id === section.id + '_ucg'
if (BANNED_HEADINGS.some((h) => heading.slug.endsWith(h))) continue
results[searchBucket(section.id)].push({
id: result.id,
type: 'heading',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
sectionType: sectionTypeBucket(section.id),
title:
section.id === 'reference'
? article.title + '.' + heading.title
: article.title + ': ' + heading.title,
url: isUncategorized
? `${section.id}/${article.id}#${heading.slug}`
: `${section.id}/${category.id}/${article.id}#${heading.slug}`,
score,
})
}
const articles = await Promise.all(
queryResults.map(async (result) => ({
score: result.score,
article: await db.db.get(
`SELECT id, title, description, categoryId, sectionId, keywords FROM articles WHERE id = ?`,
result.id
),
}))
)
for (const { score, article } of articles.filter(Boolean)) {
if (visited.has(article.id)) continue
visited.add(article.id)
const category = await db.db.get(
`SELECT id, title FROM categories WHERE categories.id = ?`,
article.categoryId
)
const section = await db.db.get(
`SELECT id, title FROM sections WHERE sections.id = ?`,
article.sectionId
)
const isUncategorized = category.id === section.id + '_ucg'
results[searchBucket(section.id)].push({
id: article.id,
type: 'article',
subtitle: isUncategorized ? section.title : `${section.title} / ${category.title}`,
sectionType: sectionTypeBucket(section.id),
title: article.title,
url: isUncategorized
? `${section.id}/${article.id}`
: `${section.id}/${category.id}/${article.id}`,
score,
})
}
Object.keys(results).forEach((section: string) => {
const scores = results[section as keyof Data['results']].map((a) => a.score)
const maxScore = Math.max(...scores)
const minScore = Math.min(...scores)
const bottomScore = minScore + (maxScore - minScore) * (section === 'apiDocs' ? 0.75 : 0.5)
results[section as keyof Data['results']]
.filter((a) => a.score > bottomScore)
.sort((a, b) => b.score - a.score)
.sort((a, b) => (b.type === 'heading' ? -1 : 1) - (a.type === 'heading' ? -1 : 1))
results[section as keyof Data['results']] = results[section as keyof Data['results']].slice(
0,
10
)
})
return new Response(
JSON.stringify({
results,
status: 'success',
}),
{
status: 200,
}
)
} catch (e: any) {
return new Response(
JSON.stringify({
results: structuredClone(SEARCH_RESULTS),
status: 'error',
error: e.message,
}),
{
status: 500,
}
)
}
}