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 + + + + )} + {/* */} + +