mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-29 03:46:58 +08:00
feat: optimize ai shortcuts position
This commit is contained in:
+3
-1
@@ -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"}
|
||||
|
||||
+72
@@ -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>
|
||||
)
|
||||
}
|
||||
+4
-38
@@ -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,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
|
||||
|
||||
+11
-35
@@ -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>
|
||||
)
|
||||
|
||||
-63
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user