Merge pull request #3591 from omnivore-app/feat/summaries

Summaries
This commit is contained in:
Jackson Harper
2024-02-29 14:35:12 +08:00
committed by GitHub
46 changed files with 2033 additions and 453 deletions

View File

@ -23,6 +23,7 @@
"@google-cloud/storage": "^7.0.1",
"@google-cloud/tasks": "^4.0.0",
"@graphql-tools/utils": "^9.1.1",
"@langchain/openai": "^0.0.14",
"@omnivore/content-handler": "1.0.0",
"@omnivore/liqe": "1.0.0",
"@omnivore/readability": "1.0.0",
@ -77,6 +78,7 @@
"ioredis": "^5.3.2",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.0.3",
"langchain": "^0.1.21",
"linkedom": "^0.14.9",
"lodash": "^4.17.21",
"luxon": "^3.2.1",

View File

@ -0,0 +1,36 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { User } from './user'
import { LibraryItem } from './library_item'
@Entity({ name: 'ai_summaries' })
export class AISummary {
@PrimaryGeneratedColumn('uuid')
id!: string
@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
user!: User
@ManyToOne(() => LibraryItem)
@JoinColumn({ name: 'library_item_id' })
libraryItem!: LibraryItem
@Column('text')
summary?: string
@Column('text')
title?: string
@Column('text')
slug?: string
@CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date
}

View File

@ -0,0 +1,79 @@
import { logger } from '../utils/logger'
import { loadSummarizationChain } from 'langchain/chains'
import { ChatOpenAI } from '@langchain/openai'
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
import { authTrx } from '../repository'
import { libraryItemRepository } from '../repository/library_item'
import { htmlToMarkdown } from '../utils/parser'
import { AISummary } from '../entity/AISummary'
import { LibraryItemState } from '../entity/library_item'
export interface AISummarizeJobData {
userId: string
promptId?: string
libraryItemId: string
}
export const AI_SUMMARIZE_JOB_NAME = 'ai-summary-job'
export const aiSummarize = async (jobData: AISummarizeJobData) => {
try {
const libraryItem = await authTrx(
async (tx) =>
tx
.withRepository(libraryItemRepository)
.findById(jobData.libraryItemId),
undefined,
jobData.userId
)
if (!libraryItem || libraryItem.state !== LibraryItemState.Succeeded) {
logger.info(
`Not ready to summarize library item job state: ${
libraryItem?.state ?? 'null'
}`
)
return
}
const llm = new ChatOpenAI({
configuration: {
apiKey: process.env.OPENAI_API_KEY,
},
})
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 2000,
})
const document = htmlToMarkdown(libraryItem.readableContent)
const docs = await textSplitter.createDocuments([document])
const chain = loadSummarizationChain(llm, {
type: 'map_reduce', // you can choose from map_reduce, stuff or refine
verbose: true, // to view the steps in the console
})
const response = await chain.call({
input_documents: docs,
})
if (typeof response.text !== 'string') {
logger.error(`AI summary did not return text`)
return
}
const summary = response.text
const _ = await authTrx(
async (t) => {
return t.getRepository(AISummary).save({
user: { id: jobData.userId },
libraryItem: { id: jobData.libraryItemId },
title: libraryItem.title,
slug: libraryItem.slug,
summary: summary,
})
},
undefined,
jobData.userId
)
} catch (err) {
console.log('error creating summary: ', err)
}
}

View File

@ -5,12 +5,18 @@ import { env } from './env'
import { ReportType } from './generated/graphql'
import { Merge } from './util'
import {
enqueueAISummarizeJob,
enqueueExportItem,
enqueueTriggerRuleJob,
enqueueWebhookJob,
} from './utils/createTask'
import { deepDelete } from './utils/helpers'
import { buildLogger } from './utils/logger'
import {
FeatureName,
findFeatureByName,
getFeatureName,
} from './services/features'
const logger = buildLogger('pubsub')
@ -82,6 +88,13 @@ export const createPubSubClient = (): PubsubClient => {
data,
})
if (await findFeatureByName(FeatureName.AISummaries, userId)) {
await enqueueAISummarizeJob({
userId,
libraryItemId,
})
}
return publish(
'entityCreated',
Buffer.from(JSON.stringify({ type, userId, ...cleanData }))

View File

@ -43,6 +43,7 @@ import { redisDataSource } from './redis_data_source'
import { CACHED_READING_POSITION_PREFIX } from './services/cached_reading_position'
import { getJobPriority } from './utils/createTask'
import { logger } from './utils/logger'
import { AI_SUMMARIZE_JOB_NAME, aiSummarize } from './jobs/ai-summarize'
export const QUEUE_NAME = 'omnivore-backend-queue'
export const JOB_VERSION = 'v001'
@ -113,6 +114,8 @@ export const createWorker = (connection: ConnectionOptions) =>
return callWebhook(job.data)
case EXPORT_ITEM_JOB_NAME:
return exportItem(job.data)
case AI_SUMMARIZE_JOB_NAME:
return aiSummarize(job.data)
case EXPORT_ALL_ITEMS_JOB_NAME:
return exportAllItems(job.data)
}

View File

@ -141,6 +141,8 @@ import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { WithDataSourcesContext } from './types'
import { updateEmailResolver } from './user'
import { getAISummary } from '../services/ai-summaries'
import { findUserFeatures, getFeatureName } from '../services/features'
/* eslint-disable @typescript-eslint/naming-convention */
type ResultResolveType = {
@ -346,6 +348,16 @@ export const functionResolvers = {
}
return undefined
},
async features(
user: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
if (!ctx.claims?.uid) {
return undefined
}
return findUserFeatures(ctx.claims.uid)
},
},
Article: {
async url(article: Article, _: unknown, ctx: WithDataSourcesContext) {
@ -485,6 +497,15 @@ export const functionResolvers = {
return []
},
async aiSummary(item: SearchItem, _: unknown, ctx: WithDataSourcesContext) {
return (
await getAISummary({
userId: ctx.uid,
libraryItemId: item.id,
idx: 'latest',
})
)?.summary
},
async highlights(
item: {
id: string

View File

@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { htmlToSpeechFile } from '@omnivore/text-to-speech-handler'
import cors from 'cors'
import express from 'express'
import { userRepository } from '../repository/user'
import { getClaimsByToken } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import { getAISummary } from '../services/ai-summaries'
export function aiSummariesRouter() {
const router = express.Router()
// Get an indexed summary for an individual library item
router.get(
'/library-item/:libraryItemId/:idx',
cors<express.Request>(corsConfig),
async (req, res) => {
const token = req?.cookies?.auth || req?.headers?.authorization
const claims = await getClaimsByToken(token)
if (!claims) {
return res.status(401).send('UNAUTHORIZED')
}
const { uid } = claims
const user = await userRepository.findById(uid)
if (!user) {
return res.status(400).send('Bad Request')
}
const libraryItemId = req.params.libraryItemId
console.log('params: ', req.params)
if (!libraryItemId) {
return res.status(400).send('Bad request - no library item id provided')
}
const idx = req.params.idx
if (!idx) {
return res.status(400).send('Bad request - no idx provided')
}
const result = await getAISummary({
userId: user.id,
idx: req.params.idx,
libraryItemId: req.params.libraryItemId,
})
return res.send({
summary: result?.summary,
})
}
)
return router
}

View File

@ -88,6 +88,7 @@ const schema = gql`
email: String
source: String
intercomHash: String
features: [String]
}
type Profile {
@ -1647,6 +1648,7 @@ const schema = gql`
previewContentType: String
links: JSON
folder: String!
aiSummary: String
}
type SearchItemEdge {

View File

@ -48,6 +48,7 @@ import {
} from './utils/auth'
import { corsConfig } from './utils/corsConfig'
import { buildLogger, buildLoggerTransport } from './utils/logger'
import { aiSummariesRouter } from './routers/ai_summary_router'
const PORT = process.env.PORT || 4000
@ -121,6 +122,7 @@ export const createApp = (): {
app.use('/api/page', pageRouter())
app.use('/api/user', userRouter())
app.use('/api/article', articleRouter())
app.use('/api/ai-summary', aiSummariesRouter())
app.use('/api/text-to-speech', textToSpeechRouter())
app.use('/api/notification', notificationRouter())
app.use('/api/integration', integrationRouter())

View File

@ -0,0 +1,34 @@
import { AISummary } from '../entity/AISummary'
import { authTrx } from '../repository'
export const getAISummary = async (data: {
userId: string
idx: string
libraryItemId: string
}): Promise<AISummary | undefined> => {
const aiSummary = await authTrx(
async (t) => {
const repo = t.getRepository(AISummary)
if (data.idx == 'latest') {
return repo.findOne({
where: {
user: { id: data.userId },
libraryItem: { id: data.libraryItemId },
},
order: { createdAt: 'DESC' },
})
} else {
return repo.findOne({
where: {
id: data.idx,
user: { id: data.userId },
libraryItem: { id: data.libraryItemId },
},
})
}
},
undefined,
data.userId
)
return aiSummary ?? undefined
}

View File

@ -7,6 +7,7 @@ import { getRepository } from '../repository'
import { logger } from '../utils/logger'
export enum FeatureName {
AISummaries = 'ai-summaries',
UltraRealisticVoice = 'ultra-realistic-voice',
}
@ -21,7 +22,6 @@ export const optInFeature = async (
if (name === FeatureName.UltraRealisticVoice) {
return optInUltraRealisticVoice(uid)
}
return undefined
}
@ -100,6 +100,16 @@ export const signFeatureToken = (
)
}
export const findUserFeatures = async (userId: string): Promise<string[]> => {
return (
await getRepository(Feature).find({
where: {
user: { id: userId },
},
})
).map((feature) => feature.name)
}
export const findFeatureByName = async (
name: FeatureName,
userId: string

View File

@ -44,6 +44,7 @@ import { CreateTaskError } from './errors'
import { stringToHash } from './helpers'
import { logger } from './logger'
import View = google.cloud.tasks.v2.Task.View
import { AISummarizeJobData, AI_SUMMARIZE_JOB_NAME } from '../jobs/ai-summarize'
// Instantiates a client.
const client = new CloudTasksClient()
@ -66,6 +67,7 @@ export const getJobPriority = (jobName: string): number => {
case TRIGGER_RULE_JOB_NAME:
case CALL_WEBHOOK_JOB_NAME:
case EXPORT_ITEM_JOB_NAME:
case AI_SUMMARIZE_JOB_NAME:
return 5
case BULK_ACTION_JOB_NAME:
case `${REFRESH_FEED_JOB_NAME}_high`:
@ -694,6 +696,18 @@ export const enqueueWebhookJob = async (data: CallWebhookJobData) => {
})
}
export const enqueueAISummarizeJob = async (data: AISummarizeJobData) => {
const queue = await getBackendQueue()
if (!queue) {
return undefined
}
return queue.add(AI_SUMMARIZE_JOB_NAME, data, {
priority: getJobPriority(AI_SUMMARIZE_JOB_NAME),
attempts: 3,
})
}
export const bulkEnqueueUpdateLabels = async (data: UpdateLabelsData[]) => {
const queue = await getBackendQueue()
if (!queue) {

View File

@ -0,0 +1,31 @@
-- Type: DO
-- Name: add_ai_summary_tables
-- Description: Add tables for generating AI summaries
BEGIN;
CREATE TABLE omnivore.ai_prompts (
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
user_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE,
prompt TEXT NOT NULL,
created_at timestamptz NOT NULL default current_timestamp
);
CREATE TABLE omnivore.ai_summaries (
id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(),
user_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE,
-- prompt_id uuid NOT NULL REFERENCES omnivore.ai_prompts ON DELETE CASCADE,
library_item_id uuid NOT NULL REFERENCES omnivore.library_item ON DELETE CASCADE,
summary TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL,
created_at timestamptz NOT NULL default current_timestamp
);
CREATE POLICY create_summary on omnivore.ai_summaries
FOR INSERT TO omnivore_user
WITH CHECK (true);
GRANT SELECT, INSERT ON omnivore.ai_summaries TO omnivore_user;
COMMIT;

View File

@ -0,0 +1,12 @@
-- Type: UNDO
-- Name: add_ai_summary_tables
-- Description: Add tables for generating AI summaries
BEGIN;
DROP TABLE omnivore.ai_summaries;
DROP TABLE omnivore.ai_prompts;
COMMIT;

View File

@ -33,6 +33,26 @@ export const Button = styled('button', {
border: '0px solid $ctaBlue',
},
},
tldr: {
gap: '10px',
display: 'flex',
alignItems: 'center',
borderRadius: '100px',
px: '10px',
py: '10px',
fontFamily: '$inter',
fontSize: '12px',
fontWeight: '500',
cursor: 'pointer',
color: '#EDEDED',
border: '1px solid $thLibraryMultiselectHover',
bg: 'transparent',
'&:hover': {
opacity: '0.6',
border: '1px solid $ctaBlue',
},
},
ctaDarkYellow: {
border: '1px solid transparent',

View File

@ -36,6 +36,7 @@ export const FormInput = styled('input', {
textIndent: '8px',
marginBottom: '2px',
height: '38px',
pl: '10px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',

View File

@ -0,0 +1,53 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class AIPromptIcon extends React.Component<IconProps> {
render() {
return (
<svg
width="31"
height="30"
viewBox="0 0 31 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.757812"
width="30"
height="30"
rx="15"
fill="#D0A3FF"
fillOpacity="0.1"
/>
<rect
x="1.25781"
y="0.5"
width="29"
height="29"
rx="14.5"
stroke="#D0A3FF"
strokeOpacity="0.3"
/>
<g>
<path
d="M11.0911 11.667L14.4244 15.0003L11.0911 18.3337"
stroke="#D0A3FF"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.7578 19.667H20.4245"
stroke="#D0A3FF"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
)
}
}

View File

@ -0,0 +1,61 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class BrowserIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M2.5 10C2.5 10.9849 2.69399 11.9602 3.0709 12.8701C3.44781 13.7801 4.00026 14.6069 4.6967 15.3033C5.39314 15.9997 6.21993 16.5522 7.12987 16.9291C8.03982 17.306 9.01509 17.5 10 17.5C10.9849 17.5 11.9602 17.306 12.8701 16.9291C13.7801 16.5522 14.6069 15.9997 15.3033 15.3033C15.9997 14.6069 16.5522 13.7801 16.9291 12.8701C17.306 11.9602 17.5 10.9849 17.5 10C17.5 8.01088 16.7098 6.10322 15.3033 4.6967C13.8968 3.29018 11.9891 2.5 10 2.5C8.01088 2.5 6.10322 3.29018 4.6967 4.6967C3.29018 6.10322 2.5 8.01088 2.5 10Z"
stroke={color}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 7.5H17"
stroke={color}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 12.5H17"
stroke={color}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.58322 2.5C8.17934 4.74968 7.43506 7.34822 7.43506 10C7.43506 12.6518 8.17934 15.2503 9.58322 17.5"
stroke={color}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.4167 2.5C11.8206 4.74968 12.5649 7.34822 12.5649 10C12.5649 12.6518 11.8206 15.2503 10.4167 17.5"
stroke={color}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs></defs>
</svg>
)
}
}

View File

@ -1,7 +1,6 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import { SpanBox } from '../LayoutPrimitives'
import React from 'react'
import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader'
@ -16,147 +15,112 @@ export const HeaderCheckboxIcon = (
switch (props.multiSelectMode) {
case 'search':
case 'visible':
return <HeaderCheckboxCheckedIcon />
return <HeaderCheckboxCheckedIcon size={17} />
case 'none':
case 'off':
return <HeaderCheckboxUncheckedIcon />
return <HeaderCheckboxUncheckedIcon size={17} />
case 'some':
return <HeaderCheckboxHalfCheckedIcon />
return <HeaderCheckboxHalfCheckedIcon size={17} />
}
}
export class HeaderCheckboxUncheckedIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
return (
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
<svg
width={size}
height={size}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
<g>
<path
d="M3.479 4.16667C3.479 3.72464 3.6546 3.30072 3.96716 2.98816C4.27972 2.67559 4.70364 2.5 5.14567 2.5H16.8123C17.2544 2.5 17.6783 2.67559 17.9908 2.98816C18.3034 3.30072 18.479 3.72464 18.479 4.16667V15.8333C18.479 16.2754 18.3034 16.6993 17.9908 17.0118C17.6783 17.3244 17.2544 17.5 16.8123 17.5H5.14567C4.70364 17.5 4.27972 17.3244 3.96716 17.0118C3.6546 16.6993 3.479 16.2754 3.479 15.8333V4.16667Z"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
stroke: 'var(--checkbox-color)',
}}
/>
<g>
<path
d="M12.5 14.1667C12.5 13.7246 12.6756 13.3007 12.9882 12.9882C13.3007 12.6756 13.7246 12.5 14.1667 12.5H25.8333C26.2754 12.5 26.6993 12.6756 27.0118 12.9882C27.3244 13.3007 27.5 13.7246 27.5 14.1667V25.8333C27.5 26.2754 27.3244 26.6993 27.0118 27.0118C26.6993 27.3244 26.2754 27.5 25.8333 27.5H14.1667C13.7246 27.5 13.3007 27.3244 12.9882 27.0118C12.6756 26.6993 12.5 26.2754 12.5 25.8333V14.1667Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
</g>
</svg>
)
}
}
export class HeaderCheckboxCheckedIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
return (
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--ring-fill': '#007AFF10',
},
}}
<svg
width={size}
height={size}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg
width="41"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
<g>
<path
d="M3.479 4.16667C3.479 3.72464 3.6546 3.30072 3.96716 2.98816C4.27972 2.67559 4.70364 2.5 5.14567 2.5H16.8123C17.2544 2.5 17.6783 2.67559 17.9908 2.98816C18.3034 3.30072 18.479 3.72464 18.479 4.16667V15.8333C18.479 16.2754 18.3034 16.6993 17.9908 17.0118C17.6783 17.3244 17.2544 17.5 16.8123 17.5H5.14567C4.70364 17.5 4.27972 17.3244 3.96716 17.0118C3.6546 16.6993 3.479 16.2754 3.479 15.8333V4.16667Z"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
stroke: 'var(--checkbox-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<g>
<path
d="M25.9341 11.6667C27.5674 11.6667 28.9007 12.9476 28.9857 14.5601L28.9899 14.7226V25.2776C28.9899 26.9109 27.7091 28.2442 26.0966 28.3292L25.9341 28.3334H15.3791C14.5967 28.3335 13.8442 28.0334 13.2764 27.4951C12.7087 26.9569 12.369 26.2213 12.3274 25.4401L12.3232 25.2776V14.7226C12.3232 13.0892 13.6041 11.7559 15.2166 11.6709L15.3791 11.6667H25.9341ZM23.7457 17.7442C23.5895 17.588 23.3775 17.5003 23.1566 17.5003C22.9356 17.5003 22.7237 17.588 22.5674 17.7442L19.8232 20.4876L18.7457 19.4109L18.6674 19.3417C18.4999 19.2122 18.2894 19.1513 18.0786 19.1714C17.8679 19.1915 17.6726 19.291 17.5326 19.4498C17.3926 19.6087 17.3183 19.8148 17.3247 20.0264C17.3312 20.2381 17.418 20.4393 17.5674 20.5892L19.2341 22.2559L19.3124 22.3251C19.4727 22.4495 19.673 22.5111 19.8755 22.4983C20.078 22.4856 20.2689 22.3994 20.4124 22.2559L23.7457 18.9226L23.8149 18.8442C23.9393 18.6839 24.0009 18.4837 23.9881 18.2812C23.9754 18.0787 23.8892 17.8877 23.7457 17.7442Z"
fill="#007AFF"
d="M7.73877 10.0004L10.0536 12.3152L14.6832 7.68555"
style={{
stroke: 'var(--checkbox-color)',
}}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
</g>
</svg>
)
}
}
export class HeaderCheckboxHalfCheckedIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
return (
<SpanBox
css={{
display: 'flex',
'--inner-color': '#007AFF',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--ring-fill': '#007AFF10',
},
}}
<svg
width={size}
height={size}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg
width="41"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
<g>
<path
d="M3.479 4.16667C3.479 3.72464 3.6546 3.30072 3.96716 2.98816C4.27972 2.67559 4.70364 2.5 5.14567 2.5H16.8123C17.2544 2.5 17.6783 2.67559 17.9908 2.98816C18.3034 3.30072 18.479 3.72464 18.479 4.16667V15.8333C18.479 16.2754 18.3034 16.6993 17.9908 17.0118C17.6783 17.3244 17.2544 17.5 16.8123 17.5H5.14567C4.70364 17.5 4.27972 17.3244 3.96716 17.0118C3.6546 16.6993 3.479 16.2754 3.479 15.8333V4.16667Z"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
stroke: 'var(--checkbox-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<g>
<path
d="M25.8332 11.7496C26.4962 11.7496 27.1321 12.013 27.6009 12.4819C28.0698 12.9507 28.3332 13.5866 28.3332 14.2496V25.9163C28.3332 26.5793 28.0698 27.2152 27.6009 27.6841C27.1321 28.1529 26.4962 28.4163 25.8332 28.4163H14.1665C13.5035 28.4163 12.8676 28.1529 12.3987 27.6841C11.9299 27.2152 11.6665 26.5793 11.6665 25.9163V14.2496C11.6665 13.5866 11.9299 12.9507 12.3987 12.4819C12.8676 12.013 13.5035 11.7496 14.1665 11.7496H25.8332ZM22.4998 19.2496H17.4998L17.4023 19.2555C17.1914 19.2806 16.998 19.3852 16.8617 19.5481C16.7254 19.711 16.6564 19.9198 16.6689 20.1318C16.6813 20.3438 16.7743 20.5431 16.9287 20.6889C17.0831 20.8347 17.2874 20.9161 17.4998 20.9163H22.4998L22.5973 20.9105C22.8082 20.8854 23.0016 20.7807 23.1379 20.6178C23.2743 20.4549 23.3433 20.2462 23.3308 20.0341C23.3184 19.8221 23.2254 19.6228 23.071 19.477C22.9165 19.3312 22.7122 19.2499 22.4998 19.2496Z"
fill="#007AFF"
/>
</g>
</svg>
</SpanBox>
<path
d="M6.979 10L14.979 10"
style={{
stroke: 'var(--checkbox-color)',
}}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
)
}
}

View File

@ -0,0 +1,111 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { SpanBox } from '../LayoutPrimitives'
import { IconProps } from './IconProps'
import React from 'react'
export class HeaderToggleTLDRIcon extends React.Component<IconProps> {
render() {
return (
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<g>
<path
d="M24.2299 16.9786H20.8659L24.4917 11.3988C24.5522 11.2981 24.5522 11.1974 24.5119 11.0967C24.4515 10.996 24.3306 10.9355 24.2299 10.9355H17.8847C17.7437 10.9355 17.6229 11.0363 17.5826 11.1773L15.4675 20.2418C15.4474 20.3224 15.4675 20.4231 15.5279 20.5036C15.5884 20.5842 15.6689 20.6245 15.7697 20.6245H18.3883L16.092 28.6818C16.0517 28.8229 16.1121 28.984 16.2531 29.0444C16.2934 29.0646 16.3337 29.0646 16.374 29.0646C16.4747 29.0646 16.5754 29.0243 16.6157 28.9236L24.4716 17.4419C24.532 17.3411 24.532 17.2203 24.4917 17.1397C24.4515 17.039 24.3507 16.9786 24.2299 16.9786Z"
style={{
stroke: 'var(--inner-color)',
}}
/>
</g>
</svg>
{/*
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<g>
<path
d="M13.3333 14.1654C13.3333 13.9444 13.4211 13.7324 13.5774 13.5761C13.7337 13.4198 13.9457 13.332 14.1667 13.332H17.5C17.721 13.332 17.933 13.4198 18.0893 13.5761C18.2455 13.7324 18.3333 13.9444 18.3333 14.1654V17.4987C18.3333 17.7197 18.2455 17.9317 18.0893 18.088C17.933 18.2442 17.721 18.332 17.5 18.332H14.1667C13.9457 18.332 13.7337 18.2442 13.5774 18.088C13.4211 17.9317 13.3333 17.7197 13.3333 17.4987V14.1654Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.6667 14.1654C21.6667 13.9444 21.7545 13.7324 21.9107 13.5761C22.067 13.4198 22.279 13.332 22.5 13.332H25.8333C26.0543 13.332 26.2663 13.4198 26.4226 13.5761C26.5789 13.7324 26.6667 13.9444 26.6667 14.1654V17.4987C26.6667 17.7197 26.5789 17.9317 26.4226 18.088C26.2663 18.2442 26.0543 18.332 25.8333 18.332H22.5C22.279 18.332 22.067 18.2442 21.9107 18.088C21.7545 17.9317 21.6667 17.7197 21.6667 17.4987V14.1654Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3333 22.5013C13.3333 22.2803 13.4211 22.0683 13.5774 21.912C13.7337 21.7558 13.9457 21.668 14.1667 21.668H17.5C17.721 21.668 17.933 21.7558 18.0893 21.912C18.2455 22.0683 18.3333 22.2803 18.3333 22.5013V25.8346C18.3333 26.0556 18.2455 26.2676 18.0893 26.4239C17.933 26.5802 17.721 26.668 17.5 26.668H14.1667C13.9457 26.668 13.7337 26.5802 13.5774 26.4239C13.4211 26.2676 13.3333 26.0556 13.3333 25.8346V22.5013Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.6667 22.5013C21.6667 22.2803 21.7545 22.0683 21.9107 21.912C22.067 21.7558 22.279 21.668 22.5 21.668H25.8333C26.0543 21.668 26.2663 21.7558 26.4226 21.912C26.5789 22.0683 26.6667 22.2803 26.6667 22.5013V25.8346C26.6667 26.0556 26.5789 26.2676 26.4226 26.4239C26.2663 26.5802 26.0543 26.668 25.8333 26.668H22.5C22.279 26.668 22.067 26.5802 21.9107 26.4239C21.7545 26.2676 21.6667 26.0556 21.6667 25.8346V22.5013Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg> */}
</SpanBox>
)
}
}

View File

@ -28,7 +28,7 @@ export const HighlightHoverActions = (props: HighlightHoverActionsProps) => {
<Box
css={{
height: '33px',
width: '135px',
minWidth: '150px',
bg: '$thBackground',
display: 'flex',

View File

@ -74,7 +74,7 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
overflow: 'hidden',
cursor: 'pointer',
'@media (max-width: 930px)': {
m: '15px',
// m: '15px',
width: 'calc(100% - 30px)',
},
}}
@ -101,7 +101,7 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
}
}}
>
{!isTouchScreenDevice() && (
{!isTouchScreenDevice() && props.multiSelectMode == 'off' && (
<Box
ref={refs.setFloating}
style={{ ...floatingStyles, zIndex: 3 }}
@ -211,7 +211,7 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
position: 'absolute',
top: 0,
left: 0,
m: '10px',
m: '12px',
lineHeight: '1',
}}
>

View File

@ -12,6 +12,7 @@ import { NotebookIcon } from '../../elements/icons/NotebookIcon'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { LabelIcon } from '../../elements/icons/LabelIcon'
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
import { BrowserIcon } from '../../elements/icons/BrowserIcon'
type LibraryHoverActionsProps = {
viewer: UserBasicData
@ -31,7 +32,7 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
overflow: 'clip',
height: '33px',
width: '184px',
width: '200px',
bg: '$thBackground',
display: 'flex',
@ -120,7 +121,10 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
event.stopPropagation()
}}
>
<Share size={21} color={theme.colors.thNotebookSubtle.toString()} />
<BrowserIcon
size={21}
color={theme.colors.thNotebookSubtle.toString()}
/>
</Button>
<CardMenu
item={props.item}

View File

@ -102,7 +102,7 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
}
}}
>
{!isTouchScreenDevice() && (
{!isTouchScreenDevice() && props.multiSelectMode == 'off' && (
<Box
ref={refs.setFloating}
style={{ ...floatingStyles, zIndex: 3 }}

View File

@ -1,6 +1,5 @@
import { DetailedHTMLProps, MetaHTMLAttributes } from 'react'
import Head from 'next/head'
import { useDarkModeListener } from '../../lib/hooks/useDarkModeListener'
import { webBaseURL } from '../../lib/appConfig'
type MetaTag = DetailedHTMLProps<
@ -27,8 +26,6 @@ function openGraphType(ogImage: string | null): string {
}
export function PageMetaData(props: PageMetaDataProps): JSX.Element {
const isDarkMode = useDarkModeListener()
return (
<Head>
<link rel="icon" href="/static/icons/favicon.ico" sizes="32x32" />

View File

@ -0,0 +1,62 @@
import { useGetAISummary } from '../../../lib/networking/queries/useGetAISummary'
import { SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { AIPromptIcon } from '../../elements/icons/AIPromotIcon'
type AISummaryProps = {
idx: string
libraryItemId: string
fontFamily: string
fontSize: number
lineHeight: number
readerFontColor: string
}
export const AISummary = (props: AISummaryProps): JSX.Element => {
const aisummary = useGetAISummary({
idx: props.idx,
libraryItemId: props.libraryItemId,
})
if (!aisummary.summary) {
return <></>
}
return (
<VStack
css={{
p: '20px',
mx: '50px',
mt: '50px',
mb: '50px',
gap: '15px',
border: '1px solid $thLibraryAISummaryBorder',
color: props.readerFontColor,
fontFamily: props.fontFamily,
fontSize: `${props.fontSize - 2}px`,
lineHeight: `${props.lineHeight}%`,
borderRadius: '3px',
background: '$thLibraryAISummaryBackground',
}}
>
<SpanBox
css={{
px: '11px',
bg: '#D0A3FF10',
border: '1px solid #D0A3FF30',
borderRadius: '4px',
color: '#D0A3FF',
fontSize: '12px',
fontFamily: '$inter',
}}
>
AI Summary
</SpanBox>
<SpanBox>{aisummary.summary}</SpanBox>
<SpanBox css={{ ml: 'auto' }}>
<AIPromptIcon />
</SpanBox>
</VStack>
)
}

View File

@ -19,6 +19,8 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Avatar } from '../../elements/Avatar'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { AISummary } from './AISummary'
import { userHasFeature } from '../../../lib/featureFlag'
type ArticleContainerProps = {
viewer: UserBasicData
@ -444,6 +446,16 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
recommendationsWithNotes={recommendationsWithNotes}
/>
)}
{userHasFeature(props.viewer, 'ai-summaries') && (
<AISummary
libraryItemId={props.article.id}
idx="latest"
fontFamily={styles.fontFamily}
fontSize={styles.fontSize}
lineHeight={styles.lineHeight}
readerFontColor={styles.readerFontColor}
/>
)}
</VStack>
<Article
articleId={props.article.id}

View File

@ -52,9 +52,10 @@ import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { ErrorSlothIcon } from '../../elements/icons/ErrorSlothIcon'
import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
import { FetchItemsError } from './FetchItemsError'
import { TLDRLayout } from './TLDRLayout'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'
export type LibraryMode = 'reads' | 'highlights' | 'tldr'
const fetchSearchResults = async (query: string, cb: any) => {
if (!query.startsWith('#')) return
@ -140,11 +141,17 @@ export function HomeFeedContainer(): JSX.Element {
}, [queryValue])
useEffect(() => {
console.log('ueryInputs.searchQuery', queryInputs.searchQuery)
if (
queryInputs.searchQuery &&
queryInputs.searchQuery?.indexOf('mode:highlights') > -1
) {
setMode('highlights')
} else if (
queryInputs.searchQuery &&
queryInputs.searchQuery?.indexOf('mode:tldr') > -1
) {
setMode('tldr')
} else {
setMode('reads')
}
@ -218,7 +225,6 @@ export function HomeFeedContainer(): JSX.Element {
const updatedArticle = { ...item }
updatedArticle.node = { ...item.node }
updatedArticle.isLoading = false
console.log(`Updating Metadata of ${item.node.slug}.`)
performActionOnItem('update-item', updatedArticle)
return
}
@ -975,17 +981,20 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
<VStack
css={{
height: '100%',
width: props.mode == 'highlights' ? '100%' : 'unset',
width: !showItems || props.mode == 'highlights' ? '100%' : 'unset',
}}
>
{props.mode != 'highlights' && (
<LibraryHeader
layout={layout}
viewer={viewerData?.me}
updateLayout={updateLayout}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
mode={props.mode}
setMode={props.setMode}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
multiSelectMode={props.multiSelectMode}
@ -1048,6 +1057,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
/>
)}
{showItems && props.mode == 'tldr' && (
<TLDRLayout viewer={viewerData?.me} layout={layout} {...props} />
)}
{props.showAddLinkModal && (
<AddLinkModal
handleLinkSubmission={props.handleLinkSubmission}

View File

@ -5,26 +5,24 @@ import { FormInput } from '../../elements/FormElements'
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { Button, IconButton } from '../../elements/Button'
import { FunnelSimple, MagnifyingGlass, Plus, X } from 'phosphor-react'
import { LayoutType } from './HomeFeedContainer'
import { FunnelSimple, X } from 'phosphor-react'
import { LayoutType, LibraryMode } from './HomeFeedContainer'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer'
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { AddBulkLabelsModal } from '../article/AddBulkLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { ArchiveIcon } from '../../elements/icons/ArchiveIcon'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { LabelIcon } from '../../elements/icons/LabelIcon'
import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
import { HeaderSearchIcon } from '../../elements/icons/HeaderSearchIcon'
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
import { HeaderToggleTLDRIcon } from '../../elements/icons/HeaderToggleTLDRIcon'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { userHasFeature } from '../../../lib/featureFlag'
import { MultiSelectControls, CheckBoxButton } from './MultiSelectControls'
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
type LibraryHeaderProps = {
export type LibraryHeaderProps = {
viewer: UserBasicData | undefined
layout: LayoutType
updateLayout: (layout: LayoutType) => void
@ -34,6 +32,9 @@ type LibraryHeaderProps = {
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void
mode: LibraryMode
setMode: (set: LibraryMode) => void
numItemsSelected: number
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
@ -81,18 +82,21 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
return (
<>
<VStack
alignment="center"
alignment="start"
distribution="start"
css={{
top: '0',
right: '0',
zIndex: 5,
px: '70px',
bg: '$thLibraryBackground',
position: 'fixed',
left: LIBRARY_LEFT_MENU_WIDTH,
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
transition: 'height 0.5s',
'@lgDown': { px: '20px' },
'@mdDown': {
px: '10px',
left: '0px',
right: '0',
},
@ -121,7 +125,6 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
>
{props.multiSelectMode !== 'off' ? (
<>
<CheckBoxButton {...props} />
<MultiSelectControls {...props} />
</>
) : (
@ -131,33 +134,7 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
)
}
const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => {
return (
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
switch (props.multiSelectMode) {
case 'off':
case 'none':
case 'some':
props.setMultiSelectMode('visible')
break
default:
props.setMultiSelectMode('off')
break
}
e.preventDefault()
}}
>
<HeaderCheckboxIcon multiSelectMode={props.multiSelectMode} />
</Button>
)
}
const HeaderControls = (props: LibraryHeaderProps): JSX.Element => {
const [showSearchBar, setShowSearchBar] = useState(false)
return (
<>
<SpanBox
@ -168,49 +145,58 @@ const HeaderControls = (props: LibraryHeaderProps): JSX.Element => {
>
<MenuHeaderButton {...props} />
</SpanBox>
<CheckBoxButton {...props} />
{showSearchBar ? (
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
) : (
<SearchBox {...props} />
<SpanBox css={{ display: 'flex', ml: 'auto', gap: '10px' }}>
{userHasFeature(props.viewer, 'ai-summaries') && (
<Button
title="TLDR Summaries"
style="plainIcon"
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
if (props.mode == 'reads') {
props.setMode('tldr')
} else {
props.setMode('reads')
}
e.preventDefault()
}}
>
<HeaderToggleTLDRIcon />
</Button>
)}
<Button
title="search"
title={
props.layout == 'GRID_LAYOUT'
? 'Switch to list layout'
: 'Switch to grid layout'
}
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
setShowSearchBar(true)
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
<HeaderSearchIcon />
{props.layout == 'LIST_LAYOUT' ? (
<HeaderToggleGridIcon />
) : (
<HeaderToggleListIcon />
)}
</Button>
)}
<Button
title={
props.layout == 'GRID_LAYOUT'
? 'Switch to list layout'
: 'Switch to grid layout'
}
style="plainIcon"
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
{props.layout == 'LIST_LAYOUT' ? (
<HeaderToggleGridIcon />
) : (
<HeaderToggleListIcon />
)}
</Button>
</SpanBox>
</>
)
}
@ -257,31 +243,15 @@ export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element {
)
}
export type SearchBoxProps = {
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
setShowSearchBar: (show: boolean) => void
compact?: boolean
onClose?: () => void
}
export function SearchBox(props: SearchBoxProps): JSX.Element {
export function SearchBox(props: LibraryHeaderProps): JSX.Element {
const inputRef = useRef<HTMLInputElement | null>(null)
const [focused, setFocused] = useState(false)
const [searchTerm, setSearchTerm] = useState(props.searchTerm ?? '')
const [isAddAction, setIsAddAction] = useState(false)
const IS_URL_REGEX =
/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/
useEffect(() => {
setSearchTerm(props.searchTerm ?? '')
}, [props.searchTerm])
useEffect(() => {
setIsAddAction(IS_URL_REGEX.test(searchTerm))
}, [searchTerm, props.searchTerm])
useKeyboardShortcuts(
searchBarCommands((action) => {
if (action === 'focusSearchBar' && inputRef.current) {
@ -301,10 +271,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
width: '100%',
maxWidth: '521px',
bg: '$thLibrarySearchbox',
borderRadius: '100px',
border: focused
? '2px solid $searchActiveOutline'
: '2px solid transparent',
borderRadius: '6px',
boxShadow: focused
? 'none'
: '0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
@ -317,251 +284,126 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
>
<HStack
alignment="center"
distribution="start"
distribution="center"
css={{
width: '53px',
height: '100%',
pl: props.compact ? '10px' : '15px',
pr: props.compact ? '5px' : '10px',
}}
onClick={(e) => {
inputRef.current?.focus()
e.preventDefault()
display: 'flex',
bg: props.multiSelectMode !== 'off' ? '$ctaBlue' : 'transparent',
borderTopLeftRadius: '6px',
borderBottomLeftRadius: '6px',
'--checkbox-color': 'var(--colors-thLibraryMultiselectCheckbox)',
'&:hover': {
bg: '$thLibraryMultiselectHover',
'--checkbox-color':
'var(--colors-thLibraryMultiselectCheckboxHover)',
},
}}
>
{(() => {
if (isAddAction) {
return (
<Plus
size={props.compact ? 15 : 20}
color={theme.colors.graySolid.toString()}
/>
)
}
return (
<MagnifyingGlass
size={props.compact ? 15 : 20}
color={theme.colors.graySolid.toString()}
/>
)
})()}
<CheckBoxButton {...props} />
</HStack>
<form
onSubmit={async (event) => {
event.preventDefault()
props.applySearchQuery(searchTerm || '')
inputRef.current?.blur()
if (props.onClose) {
props.onClose()
}
}}
style={{ width: '100%' }}
>
<FormInput
ref={inputRef}
type="text"
value={searchTerm}
autoFocus={true}
placeholder="Search keywords or labels"
onFocus={(event) => {
event.target.select()
setFocused(true)
}}
onBlur={() => {
setFocused(false)
}}
onChange={(event) => {
setSearchTerm(event.target.value)
}}
onKeyDown={(event) => {
const key = event.key.toLowerCase()
if (key == 'escape') {
event.currentTarget.blur()
}
}}
/>
</form>
<HStack
alignment="center"
distribution="start"
css={{
py: '15px',
marginLeft: 'auto',
border: focused
? '2px solid $searchActiveOutline'
: '2px solid transparent',
borderTopRightRadius: '6px',
borderBottomRightRadius: '6px',
width: '100%',
height: '100%',
}}
>
{/* <Button
css={{ padding: '4px', borderRadius: '50px', fontSize: '10px' }}
onClick={(event) => {
if (searchTerm && searchTerm.length) {
event.preventDefault()
setSearchTerm('')
props.applySearchQuery('')
} else {
props.setShowSearchBar(false)
}
<form
onSubmit={async (event) => {
event.preventDefault()
props.applySearchQuery(searchTerm || '')
inputRef.current?.blur()
}}
tabIndex={-1}
style={{ width: '100%' }}
>
clear
</Button> */}
<IconButton
style="searchButton"
onClick={(event) => {
if (searchTerm && searchTerm.length && searchTerm != 'in:inbox') {
event.preventDefault()
<FormInput
ref={inputRef}
type="text"
value={searchTerm}
autoFocus={false}
placeholder="Search keywords or labels"
onFocus={(event) => {
event.target.select()
setFocused(true)
}}
onBlur={() => {
setFocused(false)
}}
onChange={(event) => {
setSearchTerm(event.target.value)
}}
onKeyDown={(event) => {
const key = event.key.toLowerCase()
if (key == 'escape') {
event.currentTarget.blur()
}
}}
/>
</form>
<HStack
alignment="center"
css={{
py: '15px',
mr: '10px',
marginLeft: 'auto',
}}
>
<CancelSearchButton
onClick={() => {
setSearchTerm('in:inbox')
props.applySearchQuery('')
} else {
props.setShowSearchBar(false)
}
}}
tabIndex={-1}
>
<X
width={16}
height={16}
color={theme.colors.grayTextContrast.toString()}
inputRef.current?.blur()
}}
/>
</IconButton>
</HStack>
</HStack>
</HStack>
</Box>
)
}
type ControlButtonBoxProps = {
layout: LayoutType
updateLayout: (layout: LayoutType) => void
setShowInlineSearch?: (show: boolean) => void
numItemsSelected: number
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
type CancelSearchButtonProps = {
onClick: () => void
}
function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showLabelsModal, setShowLabelsModal] = useState(false)
const compact = false
const CancelSearchButton = (props: CancelSearchButtonProps): JSX.Element => {
const [color, setColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Box
<Button
title="Cancel"
style="plainIcon"
css={{
height: '38px',
width: '100%',
maxWidth: '521px',
bg: '$thLibrarySearchbox',
borderRadius: '100px',
border: '2px solid transparent',
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
p: '5px',
display: 'flex',
'&:hover': {
bg: '$ctaBlue',
borderRadius: '100px',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
onClick={(event) => {
event.preventDefault()
props.onClick()
}}
>
<HStack
alignment="center"
distribution="end"
css={{
width: '100%',
height: '100%',
gap: '15px',
pl: compact ? '10px' : '15px',
pr: compact ? '5px' : '10px',
}}
onClick={(e) => {
e.preventDefault()
}}
>
<SpanBox
css={{
fontSize: '14px',
fontFamily: '$display',
marginRight: 'auto',
}}
>
{props.numItemsSelected} items selected
</SpanBox>
<Button
title="Archive"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
props.performMultiSelectAction(BulkAction.ARCHIVE)
e.preventDefault()
}}
>
<ArchiveIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
<Button
title="Add labels"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
setShowLabelsModal(true)
e.preventDefault()
}}
>
<LabelIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
<Button
title="Delete"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
setShowConfirmDelete(true)
e.preventDefault()
}}
>
<TrashIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
{showConfirmDelete && (
<ConfirmationModal
message={`You are about to delete ${props.numItemsSelected} items. All associated notes and highlights will be deleted.`}
acceptButtonLabel={'Delete'}
onAccept={() => {
props.performMultiSelectAction(BulkAction.DELETE)
}}
onOpenChange={(open: boolean) => {
setShowConfirmDelete(false)
}}
/>
)}
{showLabelsModal && (
<AddBulkLabelsModal
bulkSetLabels={(labels: Label[]) => {
const labelIds = labels.map((l) => l.id)
props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
}}
onOpenChange={(open: boolean) => {
setShowLabelsModal(false)
}}
/>
)}
<Button
title="Cancel"
css={{ display: 'flex', mr: '10px' }}
style="plainIcon"
onClick={(event) => {
props.setMultiSelectMode('off')
}}
tabIndex={-1}
>
<X
width={20}
height={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
</HStack>
</Box>
<X width={19} height={19} color={color} />
</Button>
)
}

View File

@ -0,0 +1,301 @@
import { useState } from 'react'
import { theme } from '../../tokens/stitches.config'
import { Box, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { ArchiveIcon } from '../../elements/icons/ArchiveIcon'
import { LabelIcon } from '../../elements/icons/LabelIcon'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { AddBulkLabelsModal } from '../article/AddBulkLabelsModal'
import { X } from 'phosphor-react'
import { LibraryHeaderProps } from './LibraryHeader'
import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
import { Label } from '../../../lib/networking/fragments/labelFragment'
export const MultiSelectControls = (props: LibraryHeaderProps): JSX.Element => {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showLabelsModal, setShowLabelsModal] = useState(false)
// Don't change on immediate hover, the button has to be blurred at least once
const [hoveredOut, setHoveredOut] = useState(false)
const [hoverColor, setHoverColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
const compact = false
return (
<Box
css={{
height: '38px',
width: '100%',
maxWidth: '521px',
bg: '$thLibrarySearchbox',
borderRadius: '6px',
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
}}
onMouseLeave={(event) => {
setHoveredOut(true)
event.preventDefault()
}}
>
<HStack
alignment="center"
distribution="end"
css={{
width: '100%',
height: '100%',
pr: compact ? '5px' : '10px',
}}
onClick={(e) => {
e.preventDefault()
}}
>
<HStack
alignment="center"
distribution="center"
css={{
width: '53px',
height: '100%',
display: 'flex',
bg: props.multiSelectMode !== 'off' ? '$ctaBlue' : 'transparent',
borderTopLeftRadius: '6px',
borderBottomLeftRadius: '6px',
'--checkbox-color': 'white',
'&:hover': {
bg: hoveredOut ? '$thLibraryMultiselectHover' : '$ctaBlue',
'--checkbox-color': hoveredOut
? 'var(--colors-thLibraryMultiselectCheckboxHover)'
: 'white',
},
}}
>
<CheckBoxButton {...props} />
</HStack>
<HStack
alignment="center"
distribution="start"
css={{
gap: '15px',
pl: '15px',
border: '2px solid transparent',
width: '100%',
height: '100%',
}}
>
<SpanBox
css={{
fontSize: '14px',
fontFamily: '$display',
marginRight: 'auto',
}}
>
{props.numItemsSelected} items selected
</SpanBox>
<ArchiveButton {...props} />
<AddLabelsButton setShowLabelsModal={setShowLabelsModal} />
<RemoveItemsButton setShowConfirmDelete={setShowConfirmDelete} />
{showConfirmDelete && (
<ConfirmationModal
message={`You are about to delete ${props.numItemsSelected} items. All associated notes and highlights will be deleted.`}
acceptButtonLabel={'Delete'}
onAccept={() => {
props.performMultiSelectAction(BulkAction.DELETE)
}}
onOpenChange={(open: boolean) => {
setShowConfirmDelete(false)
}}
/>
)}
{showLabelsModal && (
<AddBulkLabelsModal
bulkSetLabels={(labels: Label[]) => {
const labelIds = labels.map((l) => l.id)
props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
}}
onOpenChange={(open: boolean) => {
setShowLabelsModal(false)
}}
/>
)}
<CancelButton {...props} />
</HStack>
</HStack>
</Box>
)
}
export const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => {
return (
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
switch (props.multiSelectMode) {
case 'off':
case 'none':
case 'some':
props.setMultiSelectMode('visible')
break
default:
props.setMultiSelectMode('off')
break
}
e.preventDefault()
}}
>
<HeaderCheckboxIcon multiSelectMode={props.multiSelectMode} />
</Button>
)
}
export const ArchiveButton = (props: LibraryHeaderProps): JSX.Element => {
const [color, setColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Archive"
css={{
p: '5px',
display: 'flex',
'&:hover': {
bg: '$ctaBlue',
borderRadius: '100px',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
style="plainIcon"
onClick={(e) => {
props.performMultiSelectAction(BulkAction.ARCHIVE)
e.preventDefault()
}}
>
<ArchiveIcon size={20} color={color} />
</Button>
)
}
type AddLabelsButtonProps = {
setShowLabelsModal: (set: boolean) => void
}
export const AddLabelsButton = (props: AddLabelsButtonProps): JSX.Element => {
const [color, setColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Add labels"
css={{
p: '5px',
display: 'flex',
'&:hover': {
bg: '$ctaBlue',
borderRadius: '100px',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
style="plainIcon"
onClick={(e) => {
props.setShowLabelsModal(true)
e.preventDefault()
}}
>
<LabelIcon size={20} color={color} />
</Button>
)
}
type RemoveItemsButtonProps = {
setShowConfirmDelete: (set: boolean) => void
}
export const RemoveItemsButton = (
props: RemoveItemsButtonProps
): JSX.Element => {
const [color, setColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Remove"
css={{
p: '5px',
display: 'flex',
'&:hover': {
bg: '$ctaBlue',
borderRadius: '100px',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
style="plainIcon"
onClick={(e) => {
props.setShowConfirmDelete(true)
e.preventDefault()
}}
>
<TrashIcon size={20} color={color} />
</Button>
)
}
export const CancelButton = (props: LibraryHeaderProps): JSX.Element => {
const [color, setColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Cancel"
css={{
p: '5px',
display: 'flex',
'&:hover': {
bg: '$ctaBlue',
borderRadius: '100px',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
style="plainIcon"
onClick={(e) => {
props.setMultiSelectMode('off')
e.preventDefault()
}}
>
<X width={19} height={19} color={color} />
</Button>
)
}

View File

@ -0,0 +1,296 @@
import { LayoutType } from './HomeFeedContainer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { LibraryItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Toaster } from 'react-hot-toast'
import TopBarProgress from 'react-topbar-progress-indicator'
import { StyledText } from '../../elements/StyledText'
import { Button } from '../../elements/Button'
import { ArchiveIcon } from '../../elements/icons/ArchiveIcon'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { BrowserIcon } from '../../elements/icons/BrowserIcon'
import { styled } from '@stitches/react'
import { siteName } from '../../patterns/LibraryCards/LibraryCardStyles'
import { theme } from '../../tokens/stitches.config'
import { DotsThree } from 'phosphor-react'
import { useState } from 'react'
type TLDRLayoutProps = {
layout: LayoutType
viewer?: UserBasicData
items: LibraryItem[]
isValidating: boolean
hasMore: boolean
totalItems: number
loadMore: () => void
}
const SiteIcon = styled('img', {
width: '22px',
height: '22px',
borderRadius: '100px',
})
export function TLDRLayout(props: TLDRLayoutProps): JSX.Element {
return (
<>
<VStack
alignment="start"
distribution="start"
css={{
mt: '30px',
gap: '50px',
height: '100%',
minHeight: '100vh',
px: '0px',
width: '100%',
maxWidth: '800px',
'@mdDown': {
p: '10px',
},
}}
>
<Toaster />
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
{props.items.map((item) => {
const source = siteName(
item.node.originalArticleUrl,
item.node.url,
item.node.siteName
)
return (
<VStack key={`tldr-${item.node.id}`} css={{ gap: '10px' }}>
<HStack
alignment="center"
distribution="start"
css={{
gap: '5px',
width: '100%',
height: '25px',
pb: 'red',
}}
>
<VStack
distribution="center"
alignment="center"
css={{
mr: '5px',
display: 'flex',
w: '22px',
h: '22px',
borderRadius: '100px',
bg: '$ctaBlue',
color: '$thTextSubtle',
}}
>
<SiteIcon src={item.node.siteIcon} />
</VStack>
{source && (
<SpanBox
css={{
display: 'flex',
fontFamily: '$inter',
fontSize: '16px',
}}
>
{item.node.siteName}
</SpanBox>
)}
{source && item.node.author && (
<SpanBox
css={{
display: 'flex',
fontFamily: '$inter',
fontSize: '16px',
}}
>
</SpanBox>
)}
{item.node.author && (
<SpanBox
css={{
display: 'flex',
fontFamily: '$inter',
fontSize: '16px',
}}
>
{item.node.author}
</SpanBox>
)}
<SpanBox css={{ ml: 'auto' }}>
<DotsThree
size={20}
color={theme.colors.thTextSubtle.toString()}
/>
</SpanBox>
</HStack>
<HStack
css={{
gap: '10px',
}}
>
<VStack
alignment="start"
distribution="start"
css={{ gap: '10px' }}
>
<SpanBox
css={{
fontFamily: '$inter',
fontWeight: '700',
fontSize: '20px',
textDecoration: 'underline',
a: {
color: '$thTLDRText',
},
}}
>
<a href={``}>{item.node.title}</a>
</SpanBox>
<SpanBox
css={{
fontFamily: '$inter',
fontWeight: '500',
fontSize: '14px',
lineHeight: '30px',
color: '$thTLDRText',
}}
>
{item.node.aiSummary}
</SpanBox>
<HStack css={{ gap: '15px', pt: '5px' }}>
<ArchiveButton />
<RemoveButton />
<OpenOriginalButton />
</HStack>
</VStack>
</HStack>
</VStack>
)
})}
<HStack
distribution="center"
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
) : (
<StyledText style="caption"></StyledText>
)}
</HStack>
</VStack>
</>
)
}
const ArchiveButton = (): JSX.Element => {
const [foreground, setForegroundColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Archive"
style="tldr"
css={{
'&:hover': {
bg: '$ctaBlue',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setForegroundColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setForegroundColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
onClick={(e) => {
// props.setShowConfirmDelete(true)
e.preventDefault()
}}
>
<ArchiveIcon size={20} color={foreground} />
</Button>
)
}
const RemoveButton = (): JSX.Element => {
const [foreground, setForegroundColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Remove"
style="tldr"
css={{
'&:hover': {
bg: '$ctaBlue',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setForegroundColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setForegroundColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
onClick={(e) => {
// props.setShowConfirmDelete(true)
e.preventDefault()
}}
>
<TrashIcon size={20} color={foreground} />
</Button>
)
}
const OpenOriginalButton = (): JSX.Element => {
const [foreground, setForegroundColor] = useState<string>(
theme.colors.thTextContrast2.toString()
)
return (
<Button
title="Open original"
style="tldr"
css={{
'&:hover': {
bg: '$ctaBlue',
opacity: 1.0,
},
}}
onMouseEnter={(event) => {
setForegroundColor('white')
event.preventDefault()
}}
onMouseLeave={(event) => {
setForegroundColor(theme.colors.thTextContrast2.toString())
event.preventDefault()
}}
onClick={(e) => {
// props.setShowConfirmDelete(true)
e.preventDefault()
}}
>
<BrowserIcon size={20} color={foreground} />
</Button>
)
}

View File

@ -27,10 +27,7 @@ import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
import { CoverImage } from '../../elements/CoverImage'
import { Shortcut } from '../../../pages/settings/shortcuts'
import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip'
import { NewsletterFlairIcon } from '../../elements/icons/NewsletterFlairIcon'
import { FeedFlairIcon } from '../../elements/icons/FeedFlairIcon'
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'

View File

@ -1,4 +1,3 @@
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
import { SETTINGS_SECTION_1, SETTINGS_SECTION_2 } from './SettingsMenu'
import {
Dropdown,

View File

@ -16,7 +16,7 @@ export const SETTINGS_SECTION_1 = [
{ name: 'Feeds', destination: '/settings/feeds' },
{ name: 'Subscriptions', destination: '/settings/subscriptions' },
{ name: 'Labels', destination: '/settings/labels' },
{ name: 'Shortcuts', destination: '/settings/shortcuts' },
// { name: 'Shortcuts', destination: '/settings/shortcuts' },
{ name: 'Saved Searches', destination: '/settings/saved-searches' },
{ name: 'Pinned Searches', destination: '/settings/pinned-searches' },
]

View File

@ -185,10 +185,16 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thLibrarySelectionColor: '#FFEA9F',
thLibraryNavigationMenuFooter: '#EFEADE',
thLibraryMenuFooterHover: '#FFFFFF',
thLibraryMultiselectHover: '#D9D9D9',
thLibraryMultiselectCheckbox: '#3D3D3D',
thLibraryMultiselectCheckboxHover: '#3D3D3D',
thTLDRText: '#434343',
thFormInput: '#EBEBEB',
thHomeIcon: '#2A2A2A',
thLabelChipForeground: '#2A2A2A', // : '#2A2A2A'
thLabelChipForeground: '#2A2A2A',
thLabelChipBackground: '#F5F5F5',
thLabelChipSelectedBorder: 'black',
thLabelChipUnselectedBorder: '#F5F5F5',
@ -218,6 +224,9 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thHighContrast: '#3D3D3D',
thHighlightBar: '#D9D9D9',
thLibraryAISummaryBorder: '#6A6968',
thLibraryAISummaryBackground: '#343434',
thFallbackImageForeground: '#2A2A2A',
thFallbackImageBackground: '#EDEDED',
@ -316,6 +325,12 @@ const darkThemeSpec = {
thLibrarySelectionColor: '#6A6968',
thLibraryNavigationMenuFooter: '#3D3D3D',
thLibraryMenuFooterHover: '#6A6968',
thLibraryMultiselectHover: '#6A6968',
thLibraryMultiselectCheckbox: 'white',
thLibraryMultiselectCheckboxHover: 'white',
thTLDRText: '#D9D9D9',
searchActiveOutline: '#866D15',
thFormInput: '#3D3D3D',
thHomeIcon: '#FFFFFF',
@ -352,6 +367,9 @@ const darkThemeSpec = {
thHighlightBar: '#6A6968',
thLibraryAISummaryBorder: '#6A6968',
thLibraryAISummaryBackground: '#343434',
thFallbackImageForeground: '#FEFFFF',
thFallbackImageBackground: '#3C3C3C',

View File

@ -1,7 +1,17 @@
import { UserBasicData } from "./networking/queries/useGetViewerQuery"
import { UserBasicData } from './networking/queries/useGetViewerQuery'
const VIP_USERS = ['jacksonh', 'satindar', 'hongbo', 'nat']
export const isVipUser = (user: UserBasicData): boolean => {
return VIP_USERS.includes(user.profile.username)
}
export const userHasFeature = (
user: UserBasicData | undefined,
feature: string
): boolean => {
if (!user) {
return false
}
return user.features.includes(feature)
}

View File

@ -3,7 +3,6 @@ import { Label } from '../networking/fragments/labelFragment'
import { showErrorToast } from '../toastHelpers'
import { setLabelsForHighlight } from '../networking/mutations/setLabelsForHighlight'
import { LabelsDispatcher } from './useSetPageLabels'
import { Highlight } from '../networking/fragments/highlightFragment'
export const useSetHighlightLabels = (
highlightId?: string

View File

@ -60,6 +60,17 @@ export function gqlFetcher(
return graphQLClient.request(query, variables, requestHeaders())
}
export function apiFetcher(path: string): Promise<unknown> {
const url = new URL(path, fetchEndpoint)
return fetch(url.toString(), {
headers: requestHeaders(),
credentials: 'include',
mode: 'cors',
}).then((result) => {
return result.json()
})
}
export function makePublicGqlFetcher(
variables?: unknown
): (query: string) => Promise<unknown> {

View File

@ -0,0 +1,41 @@
import useSWR, { Fetcher } from 'swr'
import { apiFetcher } from '../networkHelpers'
export interface AISummary {
id: string
summary: string
}
export interface AISummaryResponse {
error: any
isValidating: boolean
summary: string | undefined
}
export function useGetAISummary(params: {
idx: string
libraryItemId: string
}): AISummaryResponse {
const { idx, libraryItemId } = params
const { data, error, isValidating } = useSWR(
`/api/ai-summary/library-item/${libraryItemId}/${idx}`,
apiFetcher
)
try {
const result = data as AISummary
console.log('ai summary result: ', result)
return {
error,
isValidating,
summary: result.summary,
}
} catch (error) {
console.log('error', error)
return {
error,
isValidating: false,
summary: undefined,
}
}
}

View File

@ -95,10 +95,12 @@ export type LibraryItemNode = {
state: State
pageType: PageType
siteName?: string
siteIcon?: string
subscription?: string
readAt?: string
savedAt?: string
wordsCount?: number
aiSummary?: string
recommendations?: Recommendation[]
highlights?: Highlight[]
}
@ -172,6 +174,7 @@ export function useGetLibraryItemsQuery({
ownedByViewer
originalArticleUrl
uploadFileId
aiSummary
labels {
id
name
@ -183,6 +186,7 @@ export function useGetLibraryItemsQuery({
annotation
state
siteName
siteIcon
subscription
readAt
savedAt

View File

@ -20,6 +20,7 @@ export type UserBasicData = {
email: string
source: string
intercomHash: string
features: string[]
}
export type UserProfile = {
@ -45,6 +46,7 @@ export function useGetViewerQuery(): ViewerQueryResponse {
email
source
intercomHash
features
}
}
`

View File

@ -16,9 +16,14 @@ function LoadedContent(): JSX.Element {
pageTestId="home-page-tag"
>
<VStack
alignment="center"
alignment="start"
distribution="center"
css={{ backgroundColor: '$thLibraryBackground' }}
css={{
px: '70px',
backgroundColor: '$thLibraryBackground',
'@lgDown': { px: '20px' },
'@mdDown': { px: '10px' },
}}
>
<HomeFeedContainer />
</VStack>

View File

@ -406,6 +406,49 @@ export default function Account(): JSX.Element {
{/* <Button style="ctaDarkYellow">Upgrade</Button> */}
</VStack>
<VStack
css={{
padding: '24px',
width: '100%',
height: '100%',
bg: '$grayBg',
gap: '10px',
borderRadius: '5px',
}}
>
<StyledLabel>Beta features</StyledLabel>
{!isValidating && (
<>
{viewerData?.me?.features.map((feature) => {
return (
<StyledText
key={`feature-${feature}`}
style="footnote"
css={{ display: 'flex', gap: '5px' }}
>
<input
type="checkbox"
checked={true}
disabled={true}
></input>
{feature}
</StyledText>
)
})}
<StyledText
style="footnote"
css={{ display: 'flex', gap: '5px' }}
>
To learn more about beta features available,{' '}
<a href="https://discord.gg/h2z5rppzz9">
join the Omnivore Discord
</a>
</StyledText>
</>
)}
{/* <Button style="ctaDarkYellow">Upgrade</Button> */}
</VStack>
<VStack
css={{
padding: '24px',

View File

@ -439,3 +439,28 @@ export class GroupMembership extends BaseEntity {
@Column('boolean', { default: false })
is_admin!: boolean
}
@Entity({ name: 'features' })
export class Feature extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string
@JoinColumn({ name: 'user_id' })
@ManyToOne(() => User, (user) => user.articles, { eager: true })
user!: User
@Column('text')
name!: string
@Column('timestamp', { nullable: true })
grantedAt?: Date | null
@Column('timestamp', { nullable: true })
expiresAt?: Date | null
@Column({ type: 'timestamp', name: 'created_at' })
createdAt!: Date
@Column({ type: 'timestamp', name: 'updated_at' })
updatedAt!: Date
}

View File

@ -15,6 +15,7 @@ import {
LibraryItem,
Recommendation,
GroupMembership,
Feature,
} from './db'
import { compare, hashSync } from 'bcryptjs'
const readYamlFile = require('read-yaml-file')
@ -50,6 +51,7 @@ const ADMIN_USER_EMAIL =
},
{ resource: Recommendation, options: { parent: { name: 'Users' } } },
{ resource: GroupMembership, options: { parent: { name: 'Users' } } },
{ resource: Feature, options: { parent: { name: 'Users' } } },
],
})

397
yarn.lock
View File

@ -50,6 +50,21 @@
lodash "^4.17.21"
resize-observer-polyfill "^1.5.1"
"@anthropic-ai/sdk@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.9.1.tgz#b2d2b7bf05c90dce502c9a2e869066870f69ba88"
integrity sha512-wa1meQ2WSfoY8Uor3EdrJq0jTiZJoKoSii2ZVWRY1oN4Tlr5s59pADg9T79FTbPe1/se5c3pBeZgJL63wmuoBA==
dependencies:
"@types/node" "^18.11.18"
"@types/node-fetch" "^2.6.4"
abort-controller "^3.0.0"
agentkeepalive "^4.2.1"
digest-fetch "^1.3.0"
form-data-encoder "1.7.2"
formdata-node "^4.3.2"
node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1"
"@apollo/protobufjs@1.2.6":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.6.tgz#d601e65211e06ae1432bf5993a1a0105f2862f27"
@ -3601,6 +3616,57 @@
dependencies:
lodash "^4.17.21"
"@langchain/community@~0.0.33":
version "0.0.33"
resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.0.33.tgz#5568fe36b1e2f8947d49414d47e14a27da5b65c9"
integrity sha512-m7KfOB2t/6ZQkx29FcqHeOe+jxQZDJdRqpMsCAxRebCaIUiAwNJgRqqukQOcDsG574jhEyEMYuYDfImfXSY7aw==
dependencies:
"@langchain/core" "~0.1.36"
"@langchain/openai" "~0.0.14"
flat "^5.0.2"
langsmith "~0.1.1"
uuid "^9.0.0"
zod "^3.22.3"
"@langchain/core@~0.1.13", "@langchain/core@~0.1.36", "@langchain/core@~0.1.39":
version "0.1.39"
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.1.39.tgz#c9b993f857d935afe1b66d1cc001805f1b7db8a0"
integrity sha512-bhsMOSLPkWbVZuE3uPd9dgiOeqFwexU7IGfjWht+mWrAW9spuBtAOKOcrmxK3v5TYaKpoDbIiH641FobyD947g==
dependencies:
ansi-styles "^5.0.0"
camelcase "6"
decamelize "1.2.0"
js-tiktoken "^1.0.8"
langsmith "~0.1.7"
ml-distance "^4.0.0"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/openai@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.14.tgz#27a6ba83f6b754391868b22f3b90cd440038acf0"
integrity sha512-co6nRylPrLGY/C3JYxhHt6cxLq07P086O7K3QaZH7SFFErIN9wSzJonpvhZR07DEUq6eK6wKgh2ORxA/NcjSRQ==
dependencies:
"@langchain/core" "~0.1.13"
js-tiktoken "^1.0.7"
openai "^4.26.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@langchain/openai@~0.0.14":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.15.tgz#541271bb36066422957ac7f822ca2a45281630b1"
integrity sha512-ILecml9YopmQxfpaquYEG+KfEz7svJqpcla671J1tVngqtPwRqg9PLUOa2eDrPsunScxKUeLd8HjAGLd/eaefQ==
dependencies:
"@langchain/core" "~0.1.39"
js-tiktoken "^1.0.7"
openai "^4.26.0"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
"@lerna/child-process@7.4.1":
version "7.4.1"
resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-7.4.1.tgz#efacbbe79794ef977feb86873d853bb8708707be"
@ -6012,6 +6078,11 @@
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
"@sqltools/formatter@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
"@stitches/react@^1.2.5":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
@ -7812,6 +7883,14 @@
"@types/node" "*"
form-data "^3.0.0"
"@types/node-fetch@^2.6.4":
version "2.6.11"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
dependencies:
"@types/node" "*"
form-data "^4.0.0"
"@types/node-fetch@^2.6.6":
version "2.6.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.7.tgz#a1abe2ce24228b58ad97f99480fdcf9bbc6ab16d"
@ -7840,6 +7919,20 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.68.tgz#3155f64a961b3d8d10246c80657f9a7292e3421a"
integrity sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==
"@types/node@^18.11.18":
version "18.19.19"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.19.tgz#fe8bdbf85683a461ad685ead52a57ab2ee2315e4"
integrity sha512-qqV6hSy9zACEhQUy5CEGeuXAZN0fNjqLWRIvOXOwdFYhFoKBiY08VKR5kgchr90+TitLVhpUEb54hk4bYaArUw==
dependencies:
undici-types "~5.26.4"
"@types/node@^20.11.0":
version "20.11.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.21.tgz#ad67e65652f7be15686e2df87a38076a81c5e9c5"
integrity sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==
dependencies:
undici-types "~5.26.4"
"@types/nodemailer@^6.4.4":
version "6.4.4"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
@ -8006,6 +8099,11 @@
"@types/tough-cookie" "*"
form-data "^2.5.0"
"@types/retry@0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/retry@^0.12.0":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
@ -8231,6 +8329,11 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2"
integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==
"@types/uuid@^9.0.1":
version "9.0.8"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
"@types/voca@^1.4.0":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/voca/-/voca-1.4.2.tgz#4d2ceef3582c2a1f6a1574f71f0897400d910583"
@ -9357,6 +9460,11 @@ app-root-path@^3.0.0:
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
app-root-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86"
integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
apparatus@^0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/apparatus/-/apparatus-0.0.10.tgz#81ea756772ada77863db54ceee8202c109bdca3e"
@ -10255,7 +10363,12 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1:
base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
@ -10381,6 +10494,11 @@ binary-extensions@^2.2.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
binary-search@^1.3.5:
version "1.3.6"
resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c"
integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==
binary@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
@ -11011,6 +11129,11 @@ camelcase-keys@^7.0.0:
quick-lru "^5.1.1"
type-fest "^1.2.1"
camelcase@6, camelcase@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@ -11021,11 +11144,6 @@ camelcase@^6.0.0, camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
camelcase@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001251, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001317:
version "1.0.30001527"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz"
@ -11288,6 +11406,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
charenc@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
check-error@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@ -11830,6 +11953,11 @@ command-score@^0.1.2:
resolved "https://registry.yarnpkg.com/command-score/-/command-score-0.1.2.tgz#b986ad7e8c0beba17552a56636c44ae38363d381"
integrity sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==
commander@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^2.19.0, commander@^2.20.0, commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@ -12481,6 +12609,11 @@ cross-undici-fetch@^0.1.19:
undici "^4.9.3"
web-streams-polyfill "^3.2.0"
crypt@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@ -12790,7 +12923,7 @@ dateformat@^3.0.0, dateformat@^3.0.3:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
dayjs@1.x, dayjs@^1.10.4, dayjs@^1.11.7:
dayjs@1.x, dayjs@^1.10.4, dayjs@^1.11.7, dayjs@^1.11.9:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
@ -12841,7 +12974,7 @@ decamelize-keys@^1.1.0:
decamelize "^1.1.0"
map-obj "^1.0.0"
decamelize@^1.1.0, decamelize@^1.2.0:
decamelize@1.2.0, decamelize@^1.1.0, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@ -13260,6 +13393,14 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
digest-fetch@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661"
integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==
dependencies:
base-64 "^0.1.0"
md5 "^2.3.0"
dir-glob@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
@ -13513,6 +13654,11 @@ dotenv@^16.0.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
dotenv@^16.0.3:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
dotenv@^8.0.0, dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
@ -14783,6 +14929,11 @@ exponential-backoff@^3.1.1:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==
expr-eval@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201"
integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==
express-http-context2@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/express-http-context2/-/express-http-context2-1.0.0.tgz#58cd9fb0d233739e0dcd7aabb766d1dc74522d77"
@ -15491,6 +15642,11 @@ fork-ts-checker-webpack-plugin@^6.0.4:
semver "^7.3.2"
tapable "^1.0.0"
form-data-encoder@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
form-data-encoder@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96"
@ -15545,6 +15701,14 @@ formdata-node@^4.3.1:
node-domexception "1.0.0"
web-streams-polyfill "4.0.0-beta.1"
formdata-node@^4.3.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2"
integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
dependencies:
node-domexception "1.0.0"
web-streams-polyfill "4.0.0-beta.3"
formidable@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff"
@ -16159,7 +16323,7 @@ glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glo
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^10.2.2:
glob@^10.2.2, glob@^10.3.10:
version "10.3.10"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
@ -17759,6 +17923,11 @@ is-alphanumerical@^1.0.0:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-any-array@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-2.0.1.tgz#9233242a9c098220290aa2ec28f82ca7fa79899e"
integrity sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==
is-arguments@^1.0.4, is-arguments@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@ -17822,7 +17991,7 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-buffer@^1.0.2, is-buffer@^1.1.5:
is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@ -19150,6 +19319,13 @@ js-string-escape@^1.0.1:
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
js-tiktoken@^1.0.7, js-tiktoken@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.10.tgz#2b343ec169399dcee8f9ef9807dbd4fafd3b30dc"
integrity sha512-ZoSxbGjvGyMT13x6ACo9ebhDha/0FHdKA+OsQcMOWcm1Zs7r90Rhk5lhERLzji+3rA7EKpXCgwXcM5fF3DMpdA==
dependencies:
base64-js "^1.5.1"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -19401,6 +19577,11 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
jsonpointer@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@ -19648,6 +19829,46 @@ kuler@^2.0.0:
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
langchain@^0.1.21:
version "0.1.23"
resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.1.23.tgz#51eb49548c5514aefa2fca68509f424ead58ae04"
integrity sha512-IzvF0c+mi+6kqrC0akWOQnJ0ynwkuh4qhg5bpgr56mlQItaJimO87ujktgxrOb1xMVU7fF9Y52SNc2Kjg7ihJw==
dependencies:
"@anthropic-ai/sdk" "^0.9.1"
"@langchain/community" "~0.0.33"
"@langchain/core" "~0.1.36"
"@langchain/openai" "~0.0.14"
binary-extensions "^2.2.0"
expr-eval "^2.0.2"
js-tiktoken "^1.0.7"
js-yaml "^4.1.0"
jsonpointer "^5.0.1"
langchainhub "~0.0.8"
langsmith "~0.1.7"
ml-distance "^4.0.0"
openapi-types "^12.1.3"
p-retry "4"
uuid "^9.0.0"
yaml "^2.2.1"
zod "^3.22.4"
zod-to-json-schema "^3.22.3"
langchainhub@~0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/langchainhub/-/langchainhub-0.0.8.tgz#fd4b96dc795e22e36c1a20bad31b61b0c33d3110"
integrity sha512-Woyb8YDHgqqTOZvWIbm2CaFDGfZ4NTSyXV687AG4vXEfoNo7cGQp7nhl7wL3ehenKWmNEmcxCLgOZzW8jE6lOQ==
langsmith@~0.1.1, langsmith@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.8.tgz#a98c7c3d0ecbf6c44be4b30f3be66070c099b963"
integrity sha512-GMEPhUPmkOPUih2ho07kSMhHYpCDkavc6Zg0XgBjhLsYqYaobOxFFNyOc806jOvH2yw2tmiKLuiAdlQAVbDnHg==
dependencies:
"@types/uuid" "^9.0.1"
commander "^10.0.1"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
@ -20837,6 +21058,15 @@ md5.js@^1.3.4:
inherits "^2.0.1"
safe-buffer "^5.1.2"
md5@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
dependencies:
charenc "0.0.2"
crypt "0.0.2"
is-buffer "~1.1.6"
mdast-squeeze-paragraphs@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
@ -21829,11 +22059,52 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkdirp@^2.1.3:
version "2.1.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
mkdirp@~0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
integrity sha512-8OCq0De/h9ZxseqzCH8Kw/Filf5pF/vMI6+BH7Lu0jXz2pqYCjTAQRolSxRIi+Ax+oCCjlxoJMP0YQ4XlrQNHg==
ml-array-mean@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/ml-array-mean/-/ml-array-mean-1.1.6.tgz#d951a700dc8e3a17b3e0a583c2c64abd0c619c56"
integrity sha512-MIdf7Zc8HznwIisyiJGRH9tRigg3Yf4FldW8DxKxpCCv/g5CafTw0RRu51nojVEOXuCQC7DRVVu5c7XXO/5joQ==
dependencies:
ml-array-sum "^1.1.6"
ml-array-sum@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/ml-array-sum/-/ml-array-sum-1.1.6.tgz#d1d89c20793cd29c37b09d40e85681aa4515a955"
integrity sha512-29mAh2GwH7ZmiRnup4UyibQZB9+ZLyMShvt4cH4eTK+cL2oEMIZFnSyB3SS8MlsTh6q/w/yh48KmqLxmovN4Dw==
dependencies:
is-any-array "^2.0.0"
ml-distance-euclidean@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz#3a668d236649d1b8fec96380b9435c6f42c9a817"
integrity sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q==
ml-distance@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/ml-distance/-/ml-distance-4.0.1.tgz#4741d17a1735888c5388823762271dfe604bd019"
integrity sha512-feZ5ziXs01zhyFUUUeZV5hwc0f5JW0Sh0ckU1koZe/wdVkJdGxcP06KNQuF0WBTj8FttQUzcvQcpcrOp/XrlEw==
dependencies:
ml-array-mean "^1.1.6"
ml-distance-euclidean "^2.0.0"
ml-tree-similarity "^1.0.0"
ml-tree-similarity@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ml-tree-similarity/-/ml-tree-similarity-1.0.0.tgz#24705a107e32829e24d945e87219e892159c53f0"
integrity sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==
dependencies:
binary-search "^1.3.5"
num-sort "^2.0.0"
mocha-unfunk-reporter@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mocha-unfunk-reporter/-/mocha-unfunk-reporter-0.4.0.tgz#59eda97aec6ae6e26d7af4173490a65b7b082d20"
@ -22947,6 +23218,11 @@ nullthrows@^1.1.1:
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
num-sort@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b"
integrity sha512-1MQz1Ed8z2yckoBeSfkQHHO9K1yDRxxtotKSJ9yvcTUUxSvfvzEq5GwBrjjHEpMlq/k5gvXdmJ1SbYxWtpNoVg==
num2fraction@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@ -23365,6 +23641,26 @@ open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
openai@^4.26.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.28.0.tgz#ded00e3d98c25758b5406c9675ec27a957e00930"
integrity sha512-JM8fhcpmpGN0vrUwGquYIzdcEQHtFuom6sRCbbCM6CfzZXNuRk33G7KfeRAIfnaCxSpzrP5iHtwJzIm6biUZ2Q==
dependencies:
"@types/node" "^18.11.18"
"@types/node-fetch" "^2.6.4"
abort-controller "^3.0.0"
agentkeepalive "^4.2.1"
digest-fetch "^1.3.0"
form-data-encoder "1.7.2"
formdata-node "^4.3.2"
node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1"
openapi-types@^12.1.3:
version "12.1.3"
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
opener@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
@ -23603,7 +23899,7 @@ p-pipe@3.1.0:
resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e"
integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw==
p-queue@6.6.2:
p-queue@6.6.2, p-queue@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426"
integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==
@ -23621,6 +23917,14 @@ p-reduce@^3.0.0:
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-3.0.0.tgz#f11773794792974bd1f7a14c72934248abff4160"
integrity sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==
p-retry@4:
version "4.6.2"
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
dependencies:
"@types/retry" "0.12.0"
retry "^0.13.1"
p-retry@^4.5.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c"
@ -26047,6 +26351,14 @@ read-pkg@^7.1.0:
parse-json "^5.2.0"
type-fest "^2.0.0"
read-yaml-file@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-2.1.0.tgz#c5866712db9ef5343b4d02c2413bada53c41c4a9"
integrity sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==
dependencies:
js-yaml "^4.0.0"
strip-bom "^4.0.0"
read@1, read@^1.0.7, read@~1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
@ -26189,6 +26501,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
reflect-metadata@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74"
integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==
reflect.getprototypeof@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3"
@ -29214,7 +29531,7 @@ tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0:
tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@ -29430,6 +29747,27 @@ typeorm-naming-strategies@^4.1.0:
resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz#1ec6eb296c8d7b69bb06764d5b9083ff80e814a9"
integrity sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==
typeorm@^0.3.19:
version "0.3.20"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab"
integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==
dependencies:
"@sqltools/formatter" "^1.2.5"
app-root-path "^3.1.0"
buffer "^6.0.3"
chalk "^4.1.2"
cli-highlight "^2.1.11"
dayjs "^1.11.9"
debug "^4.3.4"
dotenv "^16.0.3"
glob "^10.3.10"
mkdirp "^2.1.3"
reflect-metadata "^0.2.1"
sha.js "^2.4.11"
tslib "^2.5.0"
uuid "^9.0.0"
yargs "^17.6.2"
typeorm@^0.3.4:
version "0.3.7"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.7.tgz#5776ed5058f0acb75d64723b39ff458d21de64c1"
@ -29468,6 +29806,11 @@ typescript@^4.4.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
ua-parser-js@^0.7.30:
version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
@ -29546,6 +29889,11 @@ underscore@^1.13.4, underscore@^1.9.1:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee"
integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==
undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici@^4.9.3:
version "4.14.1"
resolved "https://registry.yarnpkg.com/undici/-/undici-4.14.1.tgz#7633b143a8a10d6d63335e00511d071e8d52a1d9"
@ -30309,11 +30657,21 @@ web-streams-polyfill@4.0.0-beta.1:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95"
integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ==
web-streams-polyfill@4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
web-streams-polyfill@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
web-streams-polyfill@^3.2.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@ -31061,6 +31419,11 @@ yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.0.tgz#2376db1083d157f4b3a452995803dbcf43b08140"
integrity sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==
yargs-parser@20.2.4:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
@ -31207,6 +31570,16 @@ yup@^0.31.0:
property-expr "^2.0.4"
toposort "^2.0.2"
zod-to-json-schema@^3.22.3:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz#f8cc691f6043e9084375e85fb1f76ebafe253d70"
integrity sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==
zod@^3.22.3, zod@^3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
zwitch@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"