feat: optimize ai shortcuts position

This commit is contained in:
DIYgod
2025-10-17 11:50:57 +08:00
parent f100d09efd
commit 363ea4e3df
6 changed files with 91 additions and 175 deletions
@@ -45,6 +45,7 @@ import { generateAndUpdateChatTitle } from "../../utils/titleGeneration"
import { GlobalFileDropZone } from "../file/GlobalFileDropZone"
import { AIErrorFallback } from "./AIErrorFallback"
import { ChatInput } from "./ChatInput"
import { ChatShortcutsRow } from "./ChatShortcutsRow"
import { CollapsibleError } from "./CollapsibleError"
import { WelcomeScreen } from "./WelcomeScreen"
@@ -311,7 +312,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
<div className="flex min-h-0 flex-1 flex-col" ref={scrollContainerParentRef}>
<AnimatePresence>
{!hasMessages && !isLoadingHistory ? (
<WelcomeScreen onSend={handleSendMessage} centerInputOnEmpty={centerInputOnEmpty} />
<WelcomeScreen centerInputOnEmpty={centerInputOnEmpty} />
) : (
<>
<ScrollArea
@@ -386,6 +387,7 @@ const ChatInterfaceContent = ({ centerInputOnEmpty }: ChatInterfaceProps) => {
)}
>
{error && <CollapsibleError error={error} />}
<ChatShortcutsRow onSelect={(prompt) => handleSendMessage(prompt)} />
<ChatInput
onSend={handleSendMessage}
variant={!hasMessages ? "minimal" : "default"}
@@ -0,0 +1,72 @@
import { AIShortcutButton } from "@follow/components/ui/ai-shortcut-button/index.js"
import { DEFAULT_SHORTCUT_TARGETS } from "@follow/shared/settings/interface"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useAISettingValue } from "~/atoms/settings/ai"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
import { useMainEntryId } from "../../hooks/useMainEntryId"
interface ChatShortcutsRowProps {
onSelect: (prompt: string) => void
}
export const ChatShortcutsRow: React.FC<ChatShortcutsRowProps> = ({ onSelect }) => {
const { t } = useTranslation("ai")
const aiSettings = useAISettingValue()
const mainEntryId = useMainEntryId()
const showSettings = useSettingModal()
const shortcutsToDisplay = useMemo(() => {
const shortcuts = aiSettings.shortcuts ?? []
const list: typeof shortcuts = []
const entry: typeof shortcuts = []
for (const shortcut of shortcuts) {
if (!shortcut.enabled) continue
const targets =
shortcut.displayTargets && shortcut.displayTargets.length > 0
? shortcut.displayTargets
: DEFAULT_SHORTCUT_TARGETS
if (targets.includes("list")) {
list.push(shortcut)
}
if (targets.includes("entry")) {
entry.push(shortcut)
}
}
return (mainEntryId ? entry : list) ?? []
}, [aiSettings.shortcuts, mainEntryId])
const handleCustomize = useCallback(() => {
showSettings("ai")
}, [showSettings])
return (
<div className="mb-3 px-1">
<div className="flex flex-nowrap items-center gap-2 overflow-x-auto py-1">
<AIShortcutButton
onClick={handleCustomize}
animationDelay={0}
size="sm"
title={t("customize_shortcuts_title")}
>
<i className="i-mgc-add-cute-re" />
</AIShortcutButton>
{shortcutsToDisplay.map((shortcut) => (
<AIShortcutButton
key={shortcut.id}
onClick={() => onSelect(shortcut.prompt)}
animationDelay={0}
size="sm"
title={shortcut.hotkey ? `${shortcut.name} (${shortcut.hotkey})` : shortcut.name}
>
{shortcut.name}
</AIShortcutButton>
))}
</div>
</div>
)
}
@@ -1,14 +1,11 @@
import { Folo } from "@follow/components/icons/folo.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
import { useElementWidth } from "@follow/hooks"
import { DEFAULT_SHORTCUT_TARGETS } from "@follow/shared/settings/interface"
import { clsx } from "@follow/utils"
import type { EditorState } from "lexical"
import { AnimatePresence, m } from "motion/react"
import { useEffect, useMemo, useRef } from "react"
import { useEffect, useRef } from "react"
import { useTranslation } from "react-i18next"
import { useAISettingValue } from "~/atoms/settings/ai"
import { AISpline } from "~/modules/ai-chat/components/3d-models/AISpline"
import { useAttachScrollBeyond } from "../../hooks/useAttachScrollBeyond"
@@ -20,38 +17,16 @@ import { DefaultWelcomeContent, EntryWelcomeContent } from "../welcome"
import { AIChatRoot } from "./AIChatRoot"
interface WelcomeScreenProps {
onSend: (message: EditorState | string) => void
centerInputOnEmpty?: boolean
}
export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps) => {
export const WelcomeScreen = ({ centerInputOnEmpty }: WelcomeScreenProps) => {
const { t } = useTranslation("ai")
const aiSettings = useAISettingValue()
const mainEntryId = useMainEntryId()
const { todayTimelineSummaryId, canReuseTimelineSummary, hasEntryContext } =
useTimelineSummarySession()
const enabledShortcuts = aiSettings.shortcuts || []
const shouldFetchTimelineSummary = canReuseTimelineSummary
const { list: listShortcuts, entry: entryShortcuts } = useMemo(() => {
const list: typeof enabledShortcuts = []
const entry: typeof enabledShortcuts = []
for (const shortcut of enabledShortcuts) {
if (!shortcut.enabled) continue
const targets =
shortcut.displayTargets && shortcut.displayTargets.length > 0
? shortcut.displayTargets
: DEFAULT_SHORTCUT_TARGETS
if (targets.includes("list")) {
list.push(shortcut)
}
if (targets.includes("entry")) {
entry.push(shortcut)
}
}
return { list, entry }
}, [enabledShortcuts])
const { handleScroll } = useAttachScrollBeyond()
const showTimelineSummary = shouldFetchTimelineSummary
@@ -87,18 +62,9 @@ export const WelcomeScreen = ({ onSend, centerInputOnEmpty }: WelcomeScreenProps
>
<AnimatePresence mode="wait">
{hasEntryContext && mainEntryId ? (
<EntryWelcomeContent
key="entry-welcome"
entryId={mainEntryId}
onSend={onSend}
shortcuts={entryShortcuts}
/>
<EntryWelcomeContent key="entry-welcome" entryId={mainEntryId} />
) : (
<DefaultWelcomeContent
key="default-welcome"
onSend={onSend}
shortcuts={listShortcuts}
/>
<DefaultWelcomeContent key="default-welcome" />
)}
</AnimatePresence>
</div>
@@ -1,38 +1 @@
import type { AIShortcut } from "@follow/shared/settings/interface"
import type { EditorState } from "lexical"
import { m } from "motion/react"
import { useTranslation } from "react-i18next"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
import { ShortcutButtonGrid } from "./ShortcutButtonGrid"
interface DefaultWelcomeContentProps {
onSend: (message: EditorState | string) => void
shortcuts: AIShortcut[]
}
export const DefaultWelcomeContent: React.FC<DefaultWelcomeContentProps> = ({
onSend,
shortcuts,
}) => {
const { t } = useTranslation("ai")
const showSettings = useSettingModal()
return (
<m.div
initial={{ opacity: 0, y: 20, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.98 }}
className="w-full space-y-8"
>
<ShortcutButtonGrid
shortcuts={shortcuts}
onSend={onSend}
onCustomize={() => showSettings("ai")}
customizeLabel={t("customize_shortcuts")}
customizeTitle={t("customize_shortcuts_title")}
/>
</m.div>
)
}
export const DefaultWelcomeContent: React.FC = () => null
@@ -1,44 +1,20 @@
import type { AIShortcut } from "@follow/shared/settings/interface"
import type { EditorState } from "lexical"
import { m } from "motion/react"
import { useTranslation } from "react-i18next"
import { useSettingModal } from "~/modules/settings/modal/use-setting-modal-hack"
import { EntrySummaryCard } from "./EntrySummaryCard"
import { ShortcutButtonGrid } from "./ShortcutButtonGrid"
interface EntryWelcomeContentProps {
entryId: string
onSend: (message: EditorState | string) => void
shortcuts: AIShortcut[]
}
export const EntryWelcomeContent: React.FC<EntryWelcomeContentProps> = ({
entryId,
onSend,
shortcuts,
}) => {
const { t } = useTranslation("ai")
const showSettings = useSettingModal()
return (
<div className="flex w-full flex-col items-center gap-6">
export const EntryWelcomeContent: React.FC<EntryWelcomeContentProps> = ({ entryId }) => (
<div className="flex w-full flex-col items-center gap-6">
<m.div
initial={{ opacity: 0, y: 20, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.98 }}
className="w-full max-w-2xl"
>
<EntrySummaryCard entryId={entryId} />
<m.div
initial={{ opacity: 0, y: 20, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.98 }}
className="w-full max-w-2xl space-y-3"
>
<ShortcutButtonGrid
shortcuts={shortcuts}
onSend={onSend}
onCustomize={() => showSettings("ai")}
customizeLabel={t("customize_shortcuts")}
customizeTitle={t("customize_shortcuts_title")}
/>
</m.div>
</div>
)
}
</m.div>
</div>
)
@@ -1,63 +0,0 @@
import { AIShortcutButton } from "@follow/components/ui/ai-shortcut-button/index.js"
import type { AIShortcut } from "@follow/shared/settings/interface"
import type { EditorState } from "lexical"
import { useMemo } from "react"
interface ShortcutButtonGridProps {
shortcuts: AIShortcut[]
onSend: (message: EditorState | string) => void
onCustomize: () => void
customizeLabel: string
customizeTitle: string
className?: string
limit?: number
}
const baseClassName = "relative flex flex-wrap items-center justify-center gap-2"
export const ShortcutButtonGrid: React.FC<ShortcutButtonGridProps> = ({
shortcuts,
onSend,
onCustomize,
customizeLabel,
customizeTitle,
className,
limit = 6,
}) => {
const enabledShortcuts = useMemo(
() => shortcuts.filter((shortcut) => shortcut.enabled),
[shortcuts],
)
const displayShortcuts = useMemo(
() => enabledShortcuts.slice(0, limit),
[enabledShortcuts, limit],
)
const wrapperClassName = className ? `${baseClassName} ${className}` : baseClassName
return (
<div className={wrapperClassName}>
{displayShortcuts.map((shortcut, index) => (
<AIShortcutButton
key={shortcut.id}
onClick={() => onSend(shortcut.prompt)}
animationDelay={index * 0.1}
title={shortcut.hotkey ? `${shortcut.name} (${shortcut.hotkey})` : shortcut.name}
>
{shortcut.name}
</AIShortcutButton>
))}
{enabledShortcuts.length === 0 && (
<AIShortcutButton
key="customize-shortcuts"
onClick={onCustomize}
animationDelay={0}
variant="outline"
title={customizeTitle}
>
{customizeLabel}
</AIShortcutButton>
)}
</div>
)
}