feat: ai daily modal

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-07 21:34:38 +08:00
parent ff162722a9
commit 67d9559229
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
12 changed files with 343 additions and 258 deletions

View File

@ -21,6 +21,7 @@
"!unused-imports/no-unused-imports",
"!@stylistic/jsx-self-closing-comp",
"!tailwindcss/classnames-order",
"!arrow-body-style",
"*"
],

View File

@ -42,7 +42,7 @@ export function ErrorElement() {
}
return (
<div className="m-auto flex min-h-full max-w-prose select-text flex-col p-8 pt-12">
<div className="m-auto flex min-h-full max-w-prose select-text flex-col p-8 pt-24">
<div className="drag-region fixed inset-x-0 top-0 h-12" />
<div className="center flex flex-col">
<i className="i-mgc-bug-cute-re size-12 text-red-400" />

View File

@ -0,0 +1,33 @@
import { CollapseGroup } from "@renderer/components/ui/collapse"
import { useCollapseGroupItemState } from "@renderer/components/ui/collapse/hooks"
import { cn } from "@renderer/lib/utils"
import { setEntryContentPlaceholderLogoShow } from "@renderer/modules/entry-content/atoms"
import { useEffect } from "react"
import { DayOf } from "./constants"
import { DailyItem } from "./daily"
import type { DailyView } from "./types"
export const EntryPlaceholderDaily = ({
view,
className,
}: {
view: DailyView
className?: string
}) => (
<div className={cn(className, "mx-auto flex w-[75ch] flex-col gap-6")}>
<CollapseGroup>
<CtxConsumer />
<DailyItem day={DayOf.Today} view={view} />
<DailyItem day={DayOf.Yesterday} view={view} />
</CollapseGroup>
</div>
)
const CtxConsumer = () => {
const status = useCollapseGroupItemState()
const isAllCollapsed = Object.values(status).every((v) => !v)
useEffect(() => {
setEntryContentPlaceholderLogoShow(isAllCollapsed)
}, [isAllCollapsed])
return null
}

View File

@ -0,0 +1,43 @@
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@renderer/components/ui/tabs"
import { FeedViewType } from "@renderer/lib/enum"
import { DayOf } from "./constants"
import { DailyReportContent, DailyReportTitle } from "./daily"
import { useParseDailyDate } from "./hooks"
const tabs = [DayOf.Today, DayOf.Yesterday]
export const FeedDailyModalContent = () => {
const today = useParseDailyDate(DayOf.Today)
const yesterday = useParseDailyDate(DayOf.Yesterday)
return (
<Tabs defaultValue={DayOf.Today as any}>
<TabsList className="w-full">
{tabs.map((tab: any) => (
<TabsTrigger key={tab} value={tab}>
{/* {tab} */}
<DailyReportTitle {...(tab === DayOf.Today ? today : yesterday)} />
</TabsTrigger>
))}
</TabsList>
<AutoResizeHeight spring>
{tabs.map((tab: any) => (
<TabsContent key={tab} value={tab} className="mt-8">
<DailyReportContent
// TODO support other view types
view={FeedViewType.SocialMedia}
{...(tab === DayOf.Today ? today : yesterday)}
/>
</TabsContent>
))}
</AutoResizeHeight>
</Tabs>
)
}

View File

@ -0,0 +1,4 @@
export enum DayOf {
Today,
Yesterday,
}

View File

@ -0,0 +1,160 @@
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import { Card, CardHeader } from "@renderer/components/ui/card"
import { Collapse } from "@renderer/components/ui/collapse"
import { LoadingCircle } from "@renderer/components/ui/loading"
import { ScrollArea } from "@renderer/components/ui/scroll-area"
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
import { useAuthQuery } from "@renderer/hooks/common"
import { apiClient } from "@renderer/lib/api-fetch"
import { defineQuery } from "@renderer/lib/defineQuery"
import { parseMarkdown } from "@renderer/lib/parse-markdown"
import { MarkAllButton } from "@renderer/modules/entry-column/mark-all-button"
import type { FC } from "react"
import { useMemo, useState } from "react"
import { useParseDailyDate } from "./hooks"
import type { DailyItemProps, DailyView } from "./types"
export const DailyItem = ({ view, day }: DailyItemProps) => {
const { title, startDate, endDate } = useParseDailyDate(day)
return (
<Collapse
hideArrow
title={
<DailyReportTitle title={title} startDate={startDate} endDate={endDate} />
}
className="mx-auto w-full max-w-lg border-b pb-6 last:border-b-0"
>
<DailyReportContent endDate={endDate} view={view} startDate={startDate} />
</Collapse>
)
}
export const DailyReportTitle = ({
endDate,
startDate,
title,
}: {
title: string
startDate: number
endDate: number
}) => (
<div className="flex items-center justify-center gap-2 text-base">
<i className="i-mgc-magic-2-cute-re" />
<div className="font-medium">
{title}
's Top News
</div>
<Tooltip>
<TooltipTrigger asChild>
<i className="i-mgc-question-cute-re translate-y-px" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
<ul className="list-outside list-decimal text-wrap pl-6 text-left text-sm">
<li>
Here is news selected by AI from your timeline
{" ("}
{new Date(startDate).toLocaleTimeString("en-US", {
weekday: "short",
hour: "numeric",
minute: "numeric",
})}
{" "}
-
{" "}
{new Date(endDate + 1).toLocaleTimeString("en-US", {
weekday: "short",
hour: "numeric",
minute: "numeric",
})}
{") "}
that may be important to you.
</li>
<li>Update daily at 8 AM and 8 PM.</li>
</ul>
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)
export const DailyReportContent: FC<{
view: DailyView
startDate: number
endDate: number
}> = ({ endDate, startDate, view }) => {
const content = useAuthQuery(
defineQuery(["daily", view, startDate, endDate], async () => {
const res = await apiClient.ai.daily.$get({
query: {
startDate: `${+startDate}`,
view: `${view}`,
},
})
return res.data
}),
{
meta: {
persist: true,
},
},
)
const [markAllButtonRef, setMarkAllButtonRef] =
useState<HTMLButtonElement | null>(null)
const eleContent = useMemo(() => {
if (!content.data) return null
const { content: _content } = parseMarkdown(content.data)
return _content
}, [content.data])
return (
<Card className="border-none bg-transparent">
<CardHeader className="space-y-0 p-0">
<ScrollArea.ScrollArea
mask={false}
flex
viewportClassName="max-h-[calc(100vh-500px)]"
>
<AutoResizeHeight spring>
{content.isLoading ? (
<LoadingCircle size="large" className="mt-8 text-center" />
) : (
eleContent && (
<div className="prose-sm mt-4 px-6 prose-p:my-1 prose-ul:my-1 prose-ul:list-outside prose-ul:list-disc prose-li:marker:text-theme-accent">
{eleContent}
</div>
)
)}
</AutoResizeHeight>
</ScrollArea.ScrollArea>
{eleContent && (
<button
type="button"
onClick={() => {
markAllButtonRef?.click()
}}
className="!mt-4 ml-auto flex items-center rounded-lg py-1 pr-2 duration-200 hover:!bg-theme-button-hover"
>
<MarkAllButton
ref={setMarkAllButtonRef}
filter={{
startTime: startDate,
endTime: endDate,
}}
className="pointer-events-none text-[1.05rem]"
/>
<span>Mark all as read</span>
</button>
)}
</CardHeader>
</Card>
)
}

View File

@ -0,0 +1,71 @@
import { useModalStack } from "@renderer/components/ui/modal"
import { useCallback, useMemo } from "react"
import { DayOf } from "./constants"
import { FeedDailyModalContent } from "./FeedDailyModalContent"
export const useParseDailyDate = (day: DayOf) =>
useMemo(() => {
const dateObj = new Date()
const nowHour = dateObj.getHours()
let startDate: number
let endDate: number
let title: string
const today8AM = dateObj.setHours(8, 0, 0, 0)
const today8PM = dateObj.setHours(20, 0, 0, 0)
dateObj.setDate(dateObj.getDate() - 1)
const yesterday8AM = dateObj.setHours(8, 0, 0, 0)
const yesterday8PM = dateObj.setHours(20, 0, 0, 0)
dateObj.setDate(dateObj.getDate() - 1)
const dayBeforeYesterday8PM = dateObj.setHours(20, 0, 0, 0)
const isToday = day === DayOf.Today
// For index 0, get the last past 8 AM or 8 PM; for index 1, get the second last past 8 AM or 8 PM.
if (nowHour >= 20) {
if (isToday) {
endDate = today8PM - 1
startDate = today8AM
title = "Today"
} else {
endDate = today8AM - 1
startDate = yesterday8PM
title = "Last Night"
}
} else if (nowHour >= 8) {
if (isToday) {
endDate = today8AM - 1
startDate = yesterday8PM
title = "Last Night"
} else {
endDate = yesterday8PM - 1
startDate = yesterday8AM
title = "Yesterday"
}
} else {
if (isToday) {
endDate = yesterday8PM - 1
startDate = yesterday8AM
title = "Yesterday"
} else {
endDate = yesterday8AM - 1
startDate = dayBeforeYesterday8PM
title = "The Night Before Last"
}
}
return { title, startDate, endDate }
}, [day])
export const useAIDailyReportModal = () => {
const { present } = useModalStack()
return useCallback(() => {
present({
content: () => <FeedDailyModalContent />,
title: "AI Daily Report",
clickOutsideToDismiss: true,
})
}, [present])
}

View File

@ -0,0 +1,12 @@
import type { FeedViewType } from "@renderer/lib/enum"
import type { DayOf } from "./constants"
export type DailyView = Extract<
FeedViewType,
FeedViewType.Articles | FeedViewType.SocialMedia
>
export interface DailyItemProps {
view: DailyView
day: DayOf
}

View File

@ -5,7 +5,6 @@ import {
import { useWhoami } from "@renderer/atoms/user"
import { ActionButton } from "@renderer/components/ui/button"
import { DividerVertical } from "@renderer/components/ui/divider"
import { useModalStack } from "@renderer/components/ui/modal"
import { EllipsisHorizontalTextWithTooltip } from "@renderer/components/ui/typography"
import {
FEED_COLLECTION_LIST,
@ -23,7 +22,7 @@ import { useRefreshFeedMutation } from "@renderer/queries/feed"
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
import type { FC } from "react"
import { Daily } from "../entry-content/daily"
import { useAIDailyReportModal } from "../ai/ai-daily/hooks"
import { EntryHeader } from "../entry-content/header"
import { MarkAllButton } from "./mark-all-button"
@ -169,21 +168,10 @@ export const EntryListHeader: FC<{
}
const DailyReportButton: FC = () => {
const { present } = useModalStack()
const present = useAIDailyReportModal()
return (
<ActionButton
onClick={() => {
present({
title: "Daily Report",
content: () => (
<Daily
view={FeedViewType.SocialMedia}
/>
),
})
}}
onClick={present}
tooltip="Daily Report"
>
<i className="i-mgc-magic-2-cute-re" />

View File

@ -1,7 +1,7 @@
import { Slot } from "@radix-ui/react-slot"
import { FeedIcon } from "@renderer/components/feed-icon"
import { ActionButton } from "@renderer/components/ui/button"
import { RelativeTime } from "@renderer/components/ui/datetime"
import { RelativeDay, RelativeTime } from "@renderer/components/ui/datetime"
import { Media } from "@renderer/components/ui/media"
import { usePreviewMedia } from "@renderer/components/ui/media/hooks"
import { Skeleton } from "@renderer/components/ui/skeleton"
@ -121,7 +121,9 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
return (
<div className="flex origin-right scale-90 items-center gap-1">
{items
.filter((item) => !item.hide && (item.key !== "read" && item.key !== "unread"))
.filter(
(item) => !item.hide && item.key !== "read" && item.key !== "unread",
)
.map((item) => (
<ActionButton
icon={
@ -175,20 +177,26 @@ export const SocialMediaDateItem = ({
className?: string
}) => {
const dateObj = new Date(date)
const dateString = dateObj.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })
const startOfDay = new Date(dateObj.setHours(0, 0, 0, 0)).getTime()
const endOfDay = new Date(dateObj.setHours(23, 59, 59, 999)).getTime()
const view = useRouteParamsSelector((s) => s.view)
return (
<div className={cn(className, "m-auto w-[75ch] gap-3 pl-5 text-base", views[view].wideMode && "flex justify-center")}>
<div
className={cn(
className,
"m-auto w-[75ch] gap-3 pl-5 text-base",
views[view].wideMode && "flex justify-center",
)}
>
<MarkAllButton
filter={{
startTime: startOfDay,
endTime: endOfDay,
}}
/>
{dateString}
<RelativeDay date={dateObj} />
</div>
)
}

View File

@ -1,235 +0,0 @@
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
import { Card, CardHeader } from "@renderer/components/ui/card"
import { Collapse, CollapseGroup } from "@renderer/components/ui/collapse"
import {
useCollapseGroupItemState,
} from "@renderer/components/ui/collapse/hooks"
import { LoadingCircle } from "@renderer/components/ui/loading"
import { ScrollArea } from "@renderer/components/ui/scroll-area"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@renderer/components/ui/tooltip"
import { useAuthQuery } from "@renderer/hooks/common"
import { apiClient } from "@renderer/lib/api-fetch"
import { defineQuery } from "@renderer/lib/defineQuery"
import type { FeedViewType } from "@renderer/lib/enum"
import { parseMarkdown } from "@renderer/lib/parse-markdown"
import { cn } from "@renderer/lib/utils"
import { MarkAllButton } from "@renderer/modules/entry-column/mark-all-button"
import type { FC } from "react"
import { useEffect, useMemo, useState } from "react"
import { setEntryContentPlaceholderLogoShow } from "./atoms"
type DailyView = Extract<
FeedViewType,
FeedViewType.Articles | FeedViewType.SocialMedia
>
enum DayOf {
Today,
Yesterday,
}
interface DailyItemProps {
view: DailyView
day: DayOf
}
const DailyItem = ({ view, day }: DailyItemProps) => {
const dateObj = new Date()
const nowHour = dateObj.getHours()
let startDate: number
let endDate: number
let title: string
const today8AM = dateObj.setHours(8, 0, 0, 0)
const today8PM = dateObj.setHours(20, 0, 0, 0)
dateObj.setDate(dateObj.getDate() - 1)
const yesterday8AM = dateObj.setHours(8, 0, 0, 0)
const yesterday8PM = dateObj.setHours(20, 0, 0, 0)
dateObj.setDate(dateObj.getDate() - 1)
const dayBeforeYesterday8PM = dateObj.setHours(20, 0, 0, 0)
const isToday = day === DayOf.Today
// For index 0, get the last past 8 AM or 8 PM; for index 1, get the second last past 8 AM or 8 PM.
if (nowHour >= 20) {
if (isToday) {
endDate = today8PM - 1
startDate = today8AM
title = "Today"
} else {
endDate = today8AM - 1
startDate = yesterday8PM
title = "Last Night"
}
} else if (nowHour >= 8) {
if (isToday) {
endDate = today8AM - 1
startDate = yesterday8PM
title = "Last Night"
} else {
endDate = yesterday8PM - 1
startDate = yesterday8AM
title = "Yesterday"
}
} else {
if (isToday) {
endDate = yesterday8PM - 1
startDate = yesterday8AM
title = "Yesterday"
} else {
endDate = yesterday8AM - 1
startDate = dayBeforeYesterday8PM
title = "The Night Before Last"
}
}
return (
<Collapse
hideArrow
title={(
<div className="flex items-center justify-center gap-2 text-base">
<i className="i-mgc-magic-2-cute-re" />
<div className="font-medium">
{title}
's Top News
</div>
<Tooltip>
<TooltipTrigger asChild>
<i className="i-mgc-question-cute-re translate-y-px" />
</TooltipTrigger>
<TooltipContent>
<ul className="list-decimal text-wrap pl-4 text-left text-sm">
<li>
Here is news selected by AI from your timeline
{" ("}
{new Date(startDate).toLocaleTimeString("en-US", {
weekday: "short",
hour: "numeric",
minute: "numeric",
})}
{" "}
-
{" "}
{new Date(endDate + 1).toLocaleTimeString("en-US", {
weekday: "short",
hour: "numeric",
minute: "numeric",
})}
{") "}
that may be important to you.
</li>
<li>Update daily at 8 AM and 8 PM.</li>
</ul>
</TooltipContent>
</Tooltip>
</div>
)}
className="mx-auto w-full max-w-lg border-b pb-6 last:border-b-0"
>
<DailyReportContent endDate={endDate} view={view} startDate={startDate} />
</Collapse>
)
}
const DailyReportContent: FC<{
view: DailyView
startDate: number
endDate: number
}> = ({ endDate, startDate, view }) => {
const content = useAuthQuery(
defineQuery(["daily", view, startDate, endDate], async () => {
const res = await apiClient.ai.daily.$get({
query: {
startDate: `${+startDate}`,
view: `${view}`,
},
})
return res.data
}),
{
meta: {
persist: true,
},
},
)
const [markAllButtonRef, setMarkAllButtonRef] =
useState<HTMLButtonElement | null>(null)
const eleContent = useMemo(() => {
if (!content.data) return null
const { content: _content } = parseMarkdown(content.data)
return _content
}, [content.data])
return (
<Card className="border-none bg-transparent">
<CardHeader className="space-y-0 p-0">
<ScrollArea.ScrollArea
mask={false}
flex
viewportClassName="max-h-[calc(100vh-500px)]"
>
<AutoResizeHeight spring>
{content.isLoading ? (
<LoadingCircle size="large" className="mt-8 text-center" />
) : (
eleContent && (
<div className="prose-sm mt-4 px-6 prose-p:my-1 prose-ul:my-1 prose-ul:list-outside prose-ul:list-disc prose-li:marker:text-theme-accent">
{eleContent}
</div>
)
)}
</AutoResizeHeight>
</ScrollArea.ScrollArea>
{eleContent && (
<button
type="button"
onClick={() => {
markAllButtonRef?.click()
}}
className="!mt-4 ml-auto flex items-center rounded-lg py-1 pr-2 duration-200 hover:!bg-theme-button-hover"
>
<MarkAllButton
ref={setMarkAllButtonRef}
filter={{
startTime: startDate,
endTime: endDate,
}}
className="pointer-events-none text-[1.05rem]"
/>
<span>Mark all as read</span>
</button>
)}
</CardHeader>
</Card>
)
}
export const Daily = ({
view,
className,
}: {
view: DailyView
className?: string
}) => (
<div className={cn(className, "mx-auto flex w-[75ch] flex-col gap-6")}>
<CollapseGroup>
<CtxConsumer />
<DailyItem day={DayOf.Today} view={view} />
<DailyItem day={DayOf.Yesterday} view={view} />
</CollapseGroup>
</div>
)
const CtxConsumer = () => {
const status = useCollapseGroupItemState()
const isAllCollapsed = Object.values(status).every((v) => !v)
useEffect(() => {
setEntryContentPlaceholderLogoShow(isAllCollapsed)
}, [isAllCollapsed])
return null
}

View File

@ -32,9 +32,9 @@ import type { FC, ReactNode } from "react"
import { useEffect, useLayoutEffect, useState } from "react"
import { LoadingCircle } from "../../components/ui/loading"
import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
import { EntryTranslation } from "../entry-column/translation"
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
import { Daily } from "./daily"
import { EntryPlaceholderLogo } from "./entry-placeholder"
import { EntryHeader } from "./header"
import { EntryContentProvider } from "./provider"
@ -53,7 +53,7 @@ export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => {
>
<EntryPlaceholderLogo />
{feedId === ROUTE_FEED_PENDING && view === FeedViewType.Articles && (
<Daily view={view} />
<EntryPlaceholderDaily view={view} />
)}
</m.div>
)