From bbe6204b91092d65961f1b2c39eaadc5f8915074 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 27 Feb 2024 09:42:03 +0800 Subject: [PATCH 01/28] summaries --- packages/api/package.json | 1 + packages/api/src/pubsub.ts | 13 +++++++++++++ packages/api/src/queue-processor.ts | 3 +++ packages/api/src/services/features.ts | 2 +- packages/api/src/utils/createTask.ts | 14 ++++++++++++++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 1c45f926e..b8c605609 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -77,6 +77,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", diff --git a/packages/api/src/pubsub.ts b/packages/api/src/pubsub.ts index 5ca9d2693..7bf25d921 100644 --- a/packages/api/src/pubsub.ts +++ b/packages/api/src/pubsub.ts @@ -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 })) diff --git a/packages/api/src/queue-processor.ts b/packages/api/src/queue-processor.ts index cbd06787b..6cd13b8a0 100644 --- a/packages/api/src/queue-processor.ts +++ b/packages/api/src/queue-processor.ts @@ -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) } diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index e0a9b337a..3d91115c1 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -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 } diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 95777a240..e63cd6c7f 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -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) { From c20d8e6b4de013e4db52c1aed482e356114d6423 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Tue, 27 Feb 2024 18:49:21 +0800 Subject: [PATCH 02/28] Add summaries, display accont features --- packages/api/package.json | 1 + packages/api/src/entity/AISummary.ts | 36 ++ packages/api/src/jobs/ai-summarize.ts | 85 ++++ .../api/src/resolvers/function_resolvers.ts | 21 + packages/api/src/routers/ai_summary_router.ts | 90 ++++ packages/api/src/schema.ts | 2 + packages/api/src/server.ts | 2 + packages/api/src/services/ai-summaries.ts | 77 +++ packages/api/src/services/features.ts | 10 + .../0166.do.add_ai_summary_tables.sql | 31 ++ .../0166.undo.add_ai_summary_tables.sql | 12 + packages/web/components/elements/Button.tsx | 19 + .../elements/icons/AIPromotIcon.tsx | 53 ++ .../components/elements/icons/BrowserIcon.tsx | 71 +++ .../elements/icons/HeaderToggleTLDRIcon.tsx | 111 +++++ .../templates/article/AISummary.tsx | 62 +++ .../templates/article/ArticleContainer.tsx | 9 + .../templates/homeFeed/HomeFeedContainer.tsx | 16 +- .../templates/homeFeed/LibraryHeader.tsx | 78 ++- .../templates/homeFeed/TLDRLayout.tsx | 159 ++++++ .../web/components/tokens/stitches.config.ts | 6 + packages/web/lib/networking/networkHelpers.ts | 12 + .../networking/queries/useGetAISummary.tsx | 41 ++ .../queries/useGetLibraryItemsQuery.tsx | 2 + .../networking/queries/useGetViewerQuery.tsx | 2 + packages/web/pages/settings/account.tsx | 43 ++ yarn.lock | 460 +++++++++++++++++- 27 files changed, 1465 insertions(+), 46 deletions(-) create mode 100644 packages/api/src/entity/AISummary.ts create mode 100644 packages/api/src/jobs/ai-summarize.ts create mode 100644 packages/api/src/routers/ai_summary_router.ts create mode 100644 packages/api/src/services/ai-summaries.ts create mode 100755 packages/db/migrations/0166.do.add_ai_summary_tables.sql create mode 100755 packages/db/migrations/0166.undo.add_ai_summary_tables.sql create mode 100644 packages/web/components/elements/icons/AIPromotIcon.tsx create mode 100644 packages/web/components/elements/icons/BrowserIcon.tsx create mode 100644 packages/web/components/elements/icons/HeaderToggleTLDRIcon.tsx create mode 100644 packages/web/components/templates/article/AISummary.tsx create mode 100644 packages/web/components/templates/homeFeed/TLDRLayout.tsx create mode 100644 packages/web/lib/networking/queries/useGetAISummary.tsx diff --git a/packages/api/package.json b/packages/api/package.json index b8c605609..111fc6502 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -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", diff --git a/packages/api/src/entity/AISummary.ts b/packages/api/src/entity/AISummary.ts new file mode 100644 index 000000000..d43cbbb7a --- /dev/null +++ b/packages/api/src/entity/AISummary.ts @@ -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 +} diff --git a/packages/api/src/jobs/ai-summarize.ts b/packages/api/src/jobs/ai-summarize.ts new file mode 100644 index 000000000..4fe1ab798 --- /dev/null +++ b/packages/api/src/jobs/ai-summarize.ts @@ -0,0 +1,85 @@ +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) => { + console.log( + 'starting summary: *************************************************************************************' + ) + + try { + const libraryItem = await authTrx( + async (tx) => + tx + .withRepository(libraryItemRepository) + .findById(jobData.libraryItemId), + undefined, + jobData.userId + ) + if (libraryItem?.state !== LibraryItemState.Succeeded) { + logger.info( + `Not ready to summarize library item job state: ${libraryItem?.state}` + ) + return + } + + logger.info(`summarizing with openai: ${process.env.OPENAI_API_KEY}`) + 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, + }) + + const summary = response.text + console.log('SUMMARY: ', summary) + + const aiSummary = 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 + ) + + console.log('created:V ', aiSummary) + } catch (err) { + console.log('error creating summary: ', err) + } + + console.log( + 'ending summary: *************************************************************************************' + ) +} diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index b2084b3b7..1f646501a 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -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, + 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 diff --git a/packages/api/src/routers/ai_summary_router.ts b/packages/api/src/routers/ai_summary_router.ts new file mode 100644 index 000000000..3cf16f98c --- /dev/null +++ b/packages/api/src/routers/ai_summary_router.ts @@ -0,0 +1,90 @@ +/* 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 * as jwt from 'jsonwebtoken' +import { userRepository } from '../repository/user' +import { getClaimsByToken } from '../utils/auth' +import { corsConfig } from '../utils/corsConfig' +import { logger } from '../utils/logger' +import { getAISummary, getRecentAISummaries } 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(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, + }) + } + ) + + router.get( + '/recents', + cors(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 offset = parseInt(req.params.offset) || 0 + const count = Math.max(10, parseInt(req.params.count) || 10) + + const result = await getRecentAISummaries({ + user, + offset, + count, + }) + + return res.send({ + count, + offset, + summaries: result, + }) + } + ) + + return router +} diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index f3175f731..86b1f46e2 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -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 { diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 176d88172..135a9c4f9 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -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()) diff --git a/packages/api/src/services/ai-summaries.ts b/packages/api/src/services/ai-summaries.ts new file mode 100644 index 000000000..a1d94a1d1 --- /dev/null +++ b/packages/api/src/services/ai-summaries.ts @@ -0,0 +1,77 @@ +import { AISummary } from '../entity/AISummary' +import { User } from '../entity/user' +import { authTrx } from '../repository' + +export const getAISummary = async (data: { + userId: string + idx: string + libraryItemId: string +}): Promise => { + 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 +} + +// Gets an ordered list of the most recent summaries with +// the provided offset and limit +export const getRecentAISummaries = async (data: { + user: User + offset: number + count: number +}): Promise => { + const summaries = await authTrx( + async (t) => { + return await t + .getRepository(AISummary) + .createQueryBuilder() + .select('ais.user_id', 'user_id') + .addSelect('ais.summary', 'summary') + .addSelect('ais.library_item_id', 'library_item_id') + .addSelect('ais.title', 'title') + .addSelect('ais.slug', 'slug') + .from((subQuery) => { + return subQuery + .select('t.user_id', 'user_id') + .addSelect('t.library_item_id', 'library_item_id') + .addSelect('t.title', 'title') + .addSelect('t.slug', 'slug') + .addSelect('t.summary', 'summary') + .addSelect('t.created_at', 'created_at') + .addSelect( + 'ROW_NUMBER() OVER (PARTITION BY t.library_item_id ORDER BY t.created_at DESC)', + 'row_num' + ) + .from('omnivore.ai_summaries', 't') + .where('t.user_id = :userId', { userId: data.user.id }) + }, 'ais') + .skip(data.offset) + .take(data.count) + .getRawMany() + }, + undefined, + data.user.id + ) + return summaries ?? undefined +} diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 3d91115c1..699c0f66e 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -100,6 +100,16 @@ export const signFeatureToken = ( ) } +export const findUserFeatures = async (userId: string): Promise => { + return ( + await getRepository(Feature).find({ + where: { + user: { id: userId }, + }, + }) + ).map((feature) => feature.name) +} + export const findFeatureByName = async ( name: FeatureName, userId: string diff --git a/packages/db/migrations/0166.do.add_ai_summary_tables.sql b/packages/db/migrations/0166.do.add_ai_summary_tables.sql new file mode 100755 index 000000000..2ef165f23 --- /dev/null +++ b/packages/db/migrations/0166.do.add_ai_summary_tables.sql @@ -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; diff --git a/packages/db/migrations/0166.undo.add_ai_summary_tables.sql b/packages/db/migrations/0166.undo.add_ai_summary_tables.sql new file mode 100755 index 000000000..10f495a76 --- /dev/null +++ b/packages/db/migrations/0166.undo.add_ai_summary_tables.sql @@ -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; diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 54e399801..768d7dca0 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -33,6 +33,25 @@ export const Button = styled('button', { border: '0px solid $ctaBlue', }, }, + tldr: { + gap: '10px', + display: 'flex', + alignItems: 'center', + borderRadius: '5px', + px: '10px', + py: '5px', + fontFamily: '$inter', + fontSize: '12px', + fontWeight: '500', + cursor: 'pointer', + color: '#EDEDED', + border: '1px solid #6A6968', + bg: 'transparent', + '&:hover': { + opacity: '0.6', + border: '0px solid $ctaBlue', + }, + }, ctaDarkYellow: { border: '1px solid transparent', diff --git a/packages/web/components/elements/icons/AIPromotIcon.tsx b/packages/web/components/elements/icons/AIPromotIcon.tsx new file mode 100644 index 000000000..7f2fe48a1 --- /dev/null +++ b/packages/web/components/elements/icons/AIPromotIcon.tsx @@ -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 { + render() { + return ( + + + + + + + + + ) + } +} diff --git a/packages/web/components/elements/icons/BrowserIcon.tsx b/packages/web/components/elements/icons/BrowserIcon.tsx new file mode 100644 index 000000000..76ac05779 --- /dev/null +++ b/packages/web/components/elements/icons/BrowserIcon.tsx @@ -0,0 +1,71 @@ +/* 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 { + render() { + const size = (this.props.size || 26).toString() + const color = (this.props.color || '#2A2A2A').toString() + + return ( + + + + + + + + + + + ) + } +} + +// +// +// diff --git a/packages/web/components/elements/icons/HeaderToggleTLDRIcon.tsx b/packages/web/components/elements/icons/HeaderToggleTLDRIcon.tsx new file mode 100644 index 000000000..0f4e1fbcb --- /dev/null +++ b/packages/web/components/elements/icons/HeaderToggleTLDRIcon.tsx @@ -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 { + render() { + return ( + + + + + + + + {/* + + + + + + + + + */} + + ) + } +} diff --git a/packages/web/components/templates/article/AISummary.tsx b/packages/web/components/templates/article/AISummary.tsx new file mode 100644 index 000000000..ac89fdeab --- /dev/null +++ b/packages/web/components/templates/article/AISummary.tsx @@ -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 ( + + + AI Summary + + {aisummary.summary} + + + + + ) +} diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index 91e5cdb1d..404996ce6 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -19,6 +19,7 @@ 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' type ArticleContainerProps = { viewer: UserBasicData @@ -444,6 +445,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element { recommendationsWithNotes={recommendationsWithNotes} /> )} +
{ 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 } @@ -986,6 +992,8 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { applySearchQuery={(searchQuery: string) => { props.applySearchQuery(searchQuery) }} + mode={props.mode} + setMode={props.setMode} showFilterMenu={showFilterMenu} setShowFilterMenu={setShowFilterMenu} multiSelectMode={props.multiSelectMode} @@ -1048,6 +1056,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { /> )} + {showItems && props.mode == 'tldr' && ( + + )} + {props.showAddLinkModal && ( void + mode: LibraryMode + setMode: (set: LibraryMode) => void + numItemsSelected: number multiSelectMode: MultiSelectMode setMultiSelectMode: (mode: MultiSelectMode) => void @@ -186,31 +190,53 @@ const HeaderControls = (props: LibraryHeaderProps): JSX.Element => { )} - + + + + + ) } diff --git a/packages/web/components/templates/homeFeed/TLDRLayout.tsx b/packages/web/components/templates/homeFeed/TLDRLayout.tsx new file mode 100644 index 000000000..76ac9dd47 --- /dev/null +++ b/packages/web/components/templates/homeFeed/TLDRLayout.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react' +import { UploadModal } from '../UploadModal' +import { LayoutType } from './HomeFeedContainer' +import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' +import { MultiSelectMode } from './LibraryHeader' +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 { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryLegacyMenu' +import { ArchiveIcon } from '../../elements/icons/ArchiveIcon' +import { TrashIcon } from '../../elements/icons/TrashIcon' +import { BrowserIcon } from '../../elements/icons/BrowserIcon' +import CheckboxComponent from '../../elements/Checkbox' + +type TLDRLayoutProps = { + layout: LayoutType + viewer?: UserBasicData + + items: LibraryItem[] + isValidating: boolean + + hasMore: boolean + totalItems: number + + loadMore: () => void +} + +export function TLDRLayout(props: TLDRLayoutProps): JSX.Element { + return ( + <> + + + + {props.isValidating && props.items.length == 0 && } + + {props.items.map((item) => { + return ( + + + + + + + + {item.node.title} + + + {item.node.aiSummary} + + + + + + + + + ) + })} + + + {props.hasMore ? ( + + ) : ( + + )} + + + + ) +} diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 8369b93cc..bfa60aaa7 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -218,6 +218,9 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thHighContrast: '#3D3D3D', thHighlightBar: '#D9D9D9', + thLibraryAISummaryBorder: '#6A6968', + thLibraryAISummaryBackground: '#343434', + thFallbackImageForeground: '#2A2A2A', thFallbackImageBackground: '#EDEDED', @@ -352,6 +355,9 @@ const darkThemeSpec = { thHighlightBar: '#6A6968', + thLibraryAISummaryBorder: '#6A6968', + thLibraryAISummaryBackground: '#343434', + thFallbackImageForeground: '#FEFFFF', thFallbackImageBackground: '#3C3C3C', diff --git a/packages/web/lib/networking/networkHelpers.ts b/packages/web/lib/networking/networkHelpers.ts index e5ffa3325..d0895b12b 100644 --- a/packages/web/lib/networking/networkHelpers.ts +++ b/packages/web/lib/networking/networkHelpers.ts @@ -60,6 +60,18 @@ export function gqlFetcher( return graphQLClient.request(query, variables, requestHeaders()) } +export function apiFetcher(path: string): Promise { + const url = new URL(path, fetchEndpoint) + console.log('fetching: ', url) + return fetch(url, { + headers: requestHeaders(), + credentials: 'include', + mode: 'cors', + }).then((result) => { + return result.json() + }) +} + export function makePublicGqlFetcher( variables?: unknown ): (query: string) => Promise { diff --git a/packages/web/lib/networking/queries/useGetAISummary.tsx b/packages/web/lib/networking/queries/useGetAISummary.tsx new file mode 100644 index 000000000..32c337150 --- /dev/null +++ b/packages/web/lib/networking/queries/useGetAISummary.tsx @@ -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, + } + } +} diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 9f9257fcf..8bbd18b96 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -99,6 +99,7 @@ export type LibraryItemNode = { readAt?: string savedAt?: string wordsCount?: number + aiSummary?: string recommendations?: Recommendation[] highlights?: Highlight[] } @@ -172,6 +173,7 @@ export function useGetLibraryItemsQuery({ ownedByViewer originalArticleUrl uploadFileId + aiSummary labels { id name diff --git a/packages/web/lib/networking/queries/useGetViewerQuery.tsx b/packages/web/lib/networking/queries/useGetViewerQuery.tsx index ffd2303c3..88beccd8b 100644 --- a/packages/web/lib/networking/queries/useGetViewerQuery.tsx +++ b/packages/web/lib/networking/queries/useGetViewerQuery.tsx @@ -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 } } ` diff --git a/packages/web/pages/settings/account.tsx b/packages/web/pages/settings/account.tsx index 3d57d36e0..041cbdad9 100644 --- a/packages/web/pages/settings/account.tsx +++ b/packages/web/pages/settings/account.tsx @@ -406,6 +406,49 @@ export default function Account(): JSX.Element { {/* */} + + Beta features + {!isValidating && ( + <> + {viewerData?.me?.features.map((feature) => { + return ( + + + {feature} + + ) + })} + + To learn more about beta features available,{' '} + + join the Omnivore Discord + + + + )} + {/* */} + + Date: Wed, 28 Feb 2024 11:13:07 +0800 Subject: [PATCH 03/28] Use the browser icon for "open original" --- .../web/components/patterns/HighlightHoverActions.tsx | 2 +- .../patterns/LibraryCards/LibraryHoverActions.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/web/components/patterns/HighlightHoverActions.tsx b/packages/web/components/patterns/HighlightHoverActions.tsx index d027c879b..4903fa116 100644 --- a/packages/web/components/patterns/HighlightHoverActions.tsx +++ b/packages/web/components/patterns/HighlightHoverActions.tsx @@ -28,7 +28,7 @@ export const HighlightHoverActions = (props: HighlightHoverActionsProps) => { { overflow: 'clip', height: '33px', - width: '184px', + width: '200px', bg: '$thBackground', display: 'flex', @@ -120,7 +121,10 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => { event.stopPropagation() }} > - + Date: Wed, 28 Feb 2024 13:05:00 +0800 Subject: [PATCH 04/28] Convert url to string before fetching --- packages/web/lib/networking/networkHelpers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/lib/networking/networkHelpers.ts b/packages/web/lib/networking/networkHelpers.ts index d0895b12b..4e0c02e98 100644 --- a/packages/web/lib/networking/networkHelpers.ts +++ b/packages/web/lib/networking/networkHelpers.ts @@ -62,8 +62,7 @@ export function gqlFetcher( export function apiFetcher(path: string): Promise { const url = new URL(path, fetchEndpoint) - console.log('fetching: ', url) - return fetch(url, { + return fetch(url.toString(), { headers: requestHeaders(), credentials: 'include', mode: 'cors', From 955be7b91ee08c25541685b382fabf47e98de2e4 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 28 Feb 2024 13:14:09 +0800 Subject: [PATCH 05/28] Left alignment of the library items --- .../web/components/templates/homeFeed/LibraryHeader.tsx | 4 +++- packages/web/pages/home.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index af7defa9f..c981654b2 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -85,18 +85,20 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { return ( <> From a27b25706828ea9644b3df31113c9385d5109a7f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 28 Feb 2024 14:46:05 +0800 Subject: [PATCH 06/28] Fixup checkbox icons --- .../elements/icons/HeaderCheckboxIcon.tsx | 190 ++++---- .../templates/homeFeed/LibraryHeader.tsx | 417 ++++++++---------- 2 files changed, 271 insertions(+), 336 deletions(-) diff --git a/packages/web/components/elements/icons/HeaderCheckboxIcon.tsx b/packages/web/components/elements/icons/HeaderCheckboxIcon.tsx index 265b7bfd7..f1ec16ad4 100644 --- a/packages/web/components/elements/icons/HeaderCheckboxIcon.tsx +++ b/packages/web/components/elements/icons/HeaderCheckboxIcon.tsx @@ -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,116 @@ export const HeaderCheckboxIcon = ( switch (props.multiSelectMode) { case 'search': case 'visible': - return + return case 'none': case 'off': - return + return case 'some': - return + return } } export class HeaderCheckboxUncheckedIcon extends React.Component { render() { + const size = (this.props.size || 26).toString() return ( - - - + - - + + + - - - + + + ) } } export class HeaderCheckboxCheckedIcon extends React.Component { render() { + const size = (this.props.size || 26).toString() + return ( - - - + - - + + + + - - + + ) } } export class HeaderCheckboxHalfCheckedIcon extends React.Component { render() { + const size = (this.props.size || 26).toString() + return ( - - - + - - - - - + + + ) } } diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index c981654b2..ceaae5929 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -18,7 +18,6 @@ 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' @@ -97,8 +96,9 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element { left: LIBRARY_LEFT_MENU_WIDTH, height: small ? '60px' : DEFAULT_HEADER_HEIGHT, transition: 'height 0.5s', + '@lgDown': { px: '20px' }, '@mdDown': { - px: '0px', + px: '10px', left: '0px', right: '0', }, @@ -127,7 +127,6 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element { > {props.multiSelectMode !== 'off' ? ( <> - ) : ( @@ -163,7 +162,6 @@ const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => { } const HeaderControls = (props: LibraryHeaderProps): JSX.Element => { - const [showSearchBar, setShowSearchBar] = useState(false) return ( <> { > - - {showSearchBar ? ( - - ) : ( - - )} + */} - { - if (searchTerm && searchTerm.length && searchTerm != 'in:inbox') { + { + 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() + } + }} + /> + + + { event.preventDefault() setSearchTerm('in:inbox') props.applySearchQuery('') - } else { - props.setShowSearchBar(false) - } - }} - tabIndex={-1} - > - - + inputRef.current?.blur() + }} + tabIndex={-1} + > + + + ) } -type ControlButtonBoxProps = { - layout: LayoutType - updateLayout: (layout: LayoutType) => void - setShowInlineSearch?: (show: boolean) => void +// type ControlButtonBoxProps = { +// layout: LayoutType +// updateLayout: (layout: LayoutType) => void +// setShowInlineSearch?: (show: boolean) => void - numItemsSelected: number - multiSelectMode: MultiSelectMode - setMultiSelectMode: (mode: MultiSelectMode) => void +// numItemsSelected: number +// multiSelectMode: MultiSelectMode +// setMultiSelectMode: (mode: MultiSelectMode) => void - performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void -} +// performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void +// } -function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { +function MultiSelectControls(props: LibraryHeaderProps): JSX.Element { const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showLabelsModal, setShowLabelsModal] = useState(false) const compact = false @@ -480,8 +421,7 @@ function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { width: '100%', maxWidth: '521px', bg: '$thLibrarySearchbox', - borderRadius: '100px', - border: '2px solid transparent', + borderRadius: '6px', boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);', }} @@ -492,103 +432,130 @@ function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element { css={{ width: '100%', height: '100%', - gap: '15px', - pl: compact ? '10px' : '15px', pr: compact ? '5px' : '10px', }} onClick={(e) => { e.preventDefault() }} > - - {props.numItemsSelected} items selected - - - - - {showConfirmDelete && ( - { - props.performMultiSelectAction(BulkAction.DELETE) + { - setShowConfirmDelete(false) + > + {props.numItemsSelected} items selected + + + + + > + + + {showConfirmDelete && ( + { + props.performMultiSelectAction(BulkAction.DELETE) + }} + onOpenChange={(open: boolean) => { + setShowConfirmDelete(false) + }} + /> + )} + {showLabelsModal && ( + { + const labelIds = labels.map((l) => l.id) + props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds) + }} + onOpenChange={(open: boolean) => { + setShowLabelsModal(false) + }} + /> + )} + + ) From 8dd71f12aabb55dbfb2f7a682432cfef08864b00 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 28 Feb 2024 14:59:49 +0800 Subject: [PATCH 07/28] Remove extra margin on smaller screens now that the card is left aligned --- .../web/components/patterns/LibraryCards/LibraryGridCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index 98653cba6..cbe589933 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -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)', }, }} From a7688e0efc9cdb3e014d8801087f41cfa5af123d Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 28 Feb 2024 15:00:31 +0800 Subject: [PATCH 08/28] Dont show hover cards when in multiselect mode --- .../web/components/patterns/LibraryCards/LibraryGridCard.tsx | 4 ++-- .../web/components/patterns/LibraryCards/LibraryListCard.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index cbe589933..e2dc59569 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -101,7 +101,7 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element { } }} > - {!isTouchScreenDevice() && ( + {!isTouchScreenDevice() && props.multiSelectMode == 'off' && ( { position: 'absolute', top: 0, left: 0, - m: '10px', + m: '12px', lineHeight: '1', }} > diff --git a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx index eab6ca90e..00374e9f4 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx @@ -102,7 +102,7 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element { } }} > - {!isTouchScreenDevice() && ( + {!isTouchScreenDevice() && props.multiSelectMode == 'off' && ( Date: Wed, 28 Feb 2024 15:00:43 +0800 Subject: [PATCH 09/28] Only show TLDR if the user is opted in --- .../templates/homeFeed/HomeFeedContainer.tsx | 1 + .../templates/homeFeed/LibraryHeader.tsx | 61 ++++++++----------- .../templates/homeFeed/TLDRLayout.tsx | 2 +- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index dc2c0878d..604cfa33f 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -987,6 +987,7 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element { {props.mode != 'highlights' && ( { diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index ceaae5929..d05bd5ce8 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -5,7 +5,7 @@ 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 { FunnelSimple, X } from 'phosphor-react' import { LayoutType, LibraryMode } from './HomeFeedContainer' import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo' import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer' @@ -21,10 +21,13 @@ import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon' 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' export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search' type LibraryHeaderProps = { + viewer: UserBasicData | undefined + layout: LayoutType updateLayout: (layout: LayoutType) => void @@ -176,25 +179,27 @@ const HeaderControls = (props: LibraryHeaderProps): JSX.Element => { - + {props.viewer?.features.includes('ai-summaries') && ( + + )} ) } @@ -413,6 +410,8 @@ export function SearchBox(props: LibraryHeaderProps): JSX.Element { function 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 compact = false return ( @@ -426,6 +425,10 @@ function MultiSelectControls(props: LibraryHeaderProps): JSX.Element { 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() + }} > From 614b086ca4e1c5550886721ae9d99385b31ca44c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 12:01:58 +0800 Subject: [PATCH 21/28] Better hovers for header items --- .../components/elements/icons/BrowserIcon.tsx | 10 - .../templates/homeFeed/LibraryHeader.tsx | 235 +++--------------- 2 files changed, 37 insertions(+), 208 deletions(-) diff --git a/packages/web/components/elements/icons/BrowserIcon.tsx b/packages/web/components/elements/icons/BrowserIcon.tsx index 76ac05779..33db6004f 100644 --- a/packages/web/components/elements/icons/BrowserIcon.tsx +++ b/packages/web/components/elements/icons/BrowserIcon.tsx @@ -59,13 +59,3 @@ export class BrowserIcon extends React.Component { ) } } - -// -// -// diff --git a/packages/web/components/templates/homeFeed/LibraryHeader.tsx b/packages/web/components/templates/homeFeed/LibraryHeader.tsx index 37fa0d0d1..93e97ba97 100644 --- a/packages/web/components/templates/homeFeed/LibraryHeader.tsx +++ b/packages/web/components/templates/homeFeed/LibraryHeader.tsx @@ -11,22 +11,16 @@ 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 { 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 @@ -140,32 +134,6 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element { ) } -const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => { - const color = theme.colors.thLibraryMenuUnselected.toString() - return ( - - ) -} - const HeaderControls = (props: LibraryHeaderProps): JSX.Element => { return ( <> @@ -341,6 +309,8 @@ export function SearchBox(props: LibraryHeaderProps): JSX.Element { border: focused ? '2px solid $searchActiveOutline' : '2px solid transparent', + borderTopRightRadius: '6px', + borderBottomRightRadius: '6px', width: '100%', height: '100%', }} @@ -381,25 +351,17 @@ export function SearchBox(props: LibraryHeaderProps): JSX.Element { alignment="center" css={{ py: '15px', + mr: '10px', marginLeft: 'auto', }} > - { - event.preventDefault() + { setSearchTerm('in:inbox') props.applySearchQuery('') inputRef.current?.blur() }} - tabIndex={-1} - > - - + /> @@ -407,164 +369,41 @@ export function SearchBox(props: LibraryHeaderProps): JSX.Element { ) } -function 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 compact = false +type CancelSearchButtonProps = { + onClick: () => void +} +const CancelSearchButton = (props: CancelSearchButtonProps): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) return ( - { - setHoveredOut(true) + onMouseEnter={(event) => { + setColor('white') event.preventDefault() }} + onMouseLeave={(event) => { + setColor(theme.colors.thTextContrast2.toString()) + event.preventDefault() + }} + onClick={(event) => { + event.preventDefault() + props.onClick() + }} > - { - e.preventDefault() - }} - > - - - - - - {props.numItemsSelected} items selected - - - - - {showConfirmDelete && ( - { - props.performMultiSelectAction(BulkAction.DELETE) - }} - onOpenChange={(open: boolean) => { - setShowConfirmDelete(false) - }} - /> - )} - {showLabelsModal && ( - { - const labelIds = labels.map((l) => l.id) - props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds) - }} - onOpenChange={(open: boolean) => { - setShowLabelsModal(false) - }} - /> - )} - - - - + + ) } From 0640233e8aff85374c85b2dfc38e30c604510157 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 12:05:41 +0800 Subject: [PATCH 22/28] Remove unused imports --- packages/web/components/patterns/PageMetaData.tsx | 3 --- packages/web/components/templates/homeFeed/TLDRLayout.tsx | 5 ----- packages/web/components/templates/navMenu/LibraryMenu.tsx | 3 --- .../web/components/templates/navMenu/SettingsDropdown.tsx | 1 - packages/web/lib/hooks/useSetHighlightLabels.tsx | 1 - 5 files changed, 13 deletions(-) diff --git a/packages/web/components/patterns/PageMetaData.tsx b/packages/web/components/patterns/PageMetaData.tsx index 733096951..8c1f038a4 100644 --- a/packages/web/components/patterns/PageMetaData.tsx +++ b/packages/web/components/patterns/PageMetaData.tsx @@ -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 ( diff --git a/packages/web/components/templates/homeFeed/TLDRLayout.tsx b/packages/web/components/templates/homeFeed/TLDRLayout.tsx index c8fbb1dd2..05122dd39 100644 --- a/packages/web/components/templates/homeFeed/TLDRLayout.tsx +++ b/packages/web/components/templates/homeFeed/TLDRLayout.tsx @@ -1,19 +1,14 @@ -import { useState } from 'react' -import { UploadModal } from '../UploadModal' import { LayoutType } from './HomeFeedContainer' import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery' -import { MultiSelectMode } from './LibraryHeader' 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 { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryLegacyMenu' import { ArchiveIcon } from '../../elements/icons/ArchiveIcon' import { TrashIcon } from '../../elements/icons/TrashIcon' import { BrowserIcon } from '../../elements/icons/BrowserIcon' -import CheckboxComponent from '../../elements/Checkbox' type TLDRLayoutProps = { layout: LayoutType diff --git a/packages/web/components/templates/navMenu/LibraryMenu.tsx b/packages/web/components/templates/navMenu/LibraryMenu.tsx index 693030058..85baec7a1 100644 --- a/packages/web/components/templates/navMenu/LibraryMenu.tsx +++ b/packages/web/components/templates/navMenu/LibraryMenu.tsx @@ -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' diff --git a/packages/web/components/templates/navMenu/SettingsDropdown.tsx b/packages/web/components/templates/navMenu/SettingsDropdown.tsx index 11e9dbf1a..a54da2f9e 100644 --- a/packages/web/components/templates/navMenu/SettingsDropdown.tsx +++ b/packages/web/components/templates/navMenu/SettingsDropdown.tsx @@ -1,4 +1,3 @@ -import { DropdownMenu } from '@radix-ui/react-dropdown-menu' import { SETTINGS_SECTION_1, SETTINGS_SECTION_2 } from './SettingsMenu' import { Dropdown, diff --git a/packages/web/lib/hooks/useSetHighlightLabels.tsx b/packages/web/lib/hooks/useSetHighlightLabels.tsx index 2e1567d5e..71d199437 100644 --- a/packages/web/lib/hooks/useSetHighlightLabels.tsx +++ b/packages/web/lib/hooks/useSetHighlightLabels.tsx @@ -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 From 0d926b8ad12ba4e91fca44c358376b6f614783c2 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 12:08:51 +0800 Subject: [PATCH 23/28] Add features table to admin --- pkg/admin/src/db.ts | 25 +++++++++++++++++++++++++ pkg/admin/src/index.ts | 2 ++ 2 files changed, 27 insertions(+) diff --git a/pkg/admin/src/db.ts b/pkg/admin/src/db.ts index aa0133bca..25f7a2de1 100644 --- a/pkg/admin/src/db.ts +++ b/pkg/admin/src/db.ts @@ -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 +} diff --git a/pkg/admin/src/index.ts b/pkg/admin/src/index.ts index 20875919c..73740109c 100644 --- a/pkg/admin/src/index.ts +++ b/pkg/admin/src/index.ts @@ -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' } } }, ], }) From c67bd364ae64de1c50b5c4fd8b56660fe06f4bec Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 12:09:45 +0800 Subject: [PATCH 24/28] Hide shortcuts menu item --- packages/web/components/templates/navMenu/SettingsMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/components/templates/navMenu/SettingsMenu.tsx b/packages/web/components/templates/navMenu/SettingsMenu.tsx index e0bd60c5e..3a06506ce 100644 --- a/packages/web/components/templates/navMenu/SettingsMenu.tsx +++ b/packages/web/components/templates/navMenu/SettingsMenu.tsx @@ -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' }, ] From 9739d58fdfacf16cd45ba1a7f883a85cc8e5550b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 12:46:02 +0800 Subject: [PATCH 25/28] Add multi select file --- .../homeFeed/MultiSelectControls.tsx | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 packages/web/components/templates/homeFeed/MultiSelectControls.tsx diff --git a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx new file mode 100644 index 000000000..e7d3ae075 --- /dev/null +++ b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx @@ -0,0 +1,300 @@ +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' + +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( + theme.colors.thTextContrast2.toString() + ) + const compact = false + + return ( + { + setHoveredOut(true) + event.preventDefault() + }} + > + { + e.preventDefault() + }} + > + + + + + + {props.numItemsSelected} items selected + + + + + {showConfirmDelete && ( + { + props.performMultiSelectAction(BulkAction.DELETE) + }} + onOpenChange={(open: boolean) => { + setShowConfirmDelete(false) + }} + /> + )} + {showLabelsModal && ( + { + const labelIds = labels.map((l) => l.id) + props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds) + }} + onOpenChange={(open: boolean) => { + setShowLabelsModal(false) + }} + /> + )} + + + + + ) +} + +export const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => { + return ( + + ) +} + +export const ArchiveButton = (props: LibraryHeaderProps): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} + +type AddLabelsButtonProps = { + setShowLabelsModal: (set: boolean) => void +} + +export const AddLabelsButton = (props: AddLabelsButtonProps): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} + +type RemoveItemsButtonProps = { + setShowConfirmDelete: (set: boolean) => void +} + +export const RemoveItemsButton = ( + props: RemoveItemsButtonProps +): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} + +export const CancelButton = (props: LibraryHeaderProps): JSX.Element => { + const [color, setColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} From 234b24ac4daebcb78f2b3c841e365fcf2434f7b7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 14:08:01 +0800 Subject: [PATCH 26/28] Hover for TLDR buttons --- packages/web/components/elements/Button.tsx | 4 +- .../homeFeed/MultiSelectControls.tsx | 6 +- .../templates/homeFeed/TLDRLayout.tsx | 259 +++++++++++++++--- .../web/components/tokens/stitches.config.ts | 4 + .../queries/useGetLibraryItemsQuery.tsx | 2 + 5 files changed, 229 insertions(+), 46 deletions(-) diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 884544415..cb88f527d 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -46,11 +46,11 @@ export const Button = styled('button', { fontWeight: '500', cursor: 'pointer', color: '#EDEDED', - border: '1px solid #6A6968', + border: '1px solid $thLibraryMultiselectHover', bg: 'transparent', '&:hover': { opacity: '0.6', - border: '0px solid $ctaBlue', + border: '1px solid $ctaBlue', }, }, diff --git a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx index e7d3ae075..143efe673 100644 --- a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx +++ b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx @@ -194,7 +194,7 @@ export const AddLabelsButton = (props: AddLabelsButtonProps): JSX.Element => { ) return ( - - - - - + + {item.node.title} + + + {item.node.aiSummary} + + + + + + + + + ) })} @@ -118,3 +199,99 @@ export function TLDRLayout(props: TLDRLayoutProps): JSX.Element { ) } + +const ArchiveButton = (): JSX.Element => { + const [foreground, setForegroundColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} + +const RemoveButton = (): JSX.Element => { + const [foreground, setForegroundColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} + +const OpenOriginalButton = (): JSX.Element => { + const [foreground, setForegroundColor] = useState( + theme.colors.thTextContrast2.toString() + ) + return ( + + ) +} diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index dbe31d23a..d863bf16d 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -189,6 +189,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } = thLibraryMultiselectCheckbox: '#3D3D3D', thLibraryMultiselectCheckboxHover: '#3D3D3D', + thTLDRText: '#434343', + thFormInput: '#EBEBEB', thHomeIcon: '#2A2A2A', @@ -327,6 +329,8 @@ const darkThemeSpec = { thLibraryMultiselectCheckbox: 'white', thLibraryMultiselectCheckboxHover: 'white', + thTLDRText: '#D9D9D9', + searchActiveOutline: '#866D15', thFormInput: '#3D3D3D', thHomeIcon: '#FFFFFF', diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index 8bbd18b96..5cdb59843 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -95,6 +95,7 @@ export type LibraryItemNode = { state: State pageType: PageType siteName?: string + siteIcon?: string subscription?: string readAt?: string savedAt?: string @@ -185,6 +186,7 @@ export function useGetLibraryItemsQuery({ annotation state siteName + siteIcon subscription readAt savedAt From 0930548f3121c936eb850e4eeba33b5f468db0aa Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 29 Feb 2024 14:20:41 +0800 Subject: [PATCH 27/28] Move the key --- packages/web/components/templates/homeFeed/TLDRLayout.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web/components/templates/homeFeed/TLDRLayout.tsx b/packages/web/components/templates/homeFeed/TLDRLayout.tsx index 47e43dcf4..53eafdc00 100644 --- a/packages/web/components/templates/homeFeed/TLDRLayout.tsx +++ b/packages/web/components/templates/homeFeed/TLDRLayout.tsx @@ -64,7 +64,7 @@ export function TLDRLayout(props: TLDRLayoutProps): JSX.Element { item.node.siteName ) return ( - + Date: Thu, 29 Feb 2024 14:21:52 +0800 Subject: [PATCH 28/28] Add label import --- .../web/components/templates/homeFeed/MultiSelectControls.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx index 143efe673..4361bef88 100644 --- a/packages/web/components/templates/homeFeed/MultiSelectControls.tsx +++ b/packages/web/components/templates/homeFeed/MultiSelectControls.tsx @@ -11,6 +11,7 @@ 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)