diff --git a/crates/agent-gateway/web/src/app/GatewayApp.tsx b/crates/agent-gateway/web/src/app/GatewayApp.tsx index 9ffb730c..085f1633 100644 --- a/crates/agent-gateway/web/src/app/GatewayApp.tsx +++ b/crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -3285,6 +3285,9 @@ export default function GatewayApp() { composerRef.current?.insertGitFileMention(file); composerRef.current?.focus(); }, []); + const handleEmptyStateSuggestion = useCallback((text: string) => { + composerRef.current?.typeText(text); + }, []); const handleRightDockClose = useCallback(() => { setRightDockOpen(false); }, []); @@ -3768,6 +3771,7 @@ export default function GatewayApp() { gitClient={gitClient} onLoadUploadedImagePreview={handleLoadUploadedImagePreview} onResendFromEdit={handleResendFromEdit} + onSuggestionSelect={handleEmptyStateSuggestion} /> {conversationOpenState.showOverlay ? ( diff --git a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx index 28fe0cd8..afcbb5fc 100644 --- a/crates/agent-gateway/web/src/components/GatewayTranscript.tsx +++ b/crates/agent-gateway/web/src/components/GatewayTranscript.tsx @@ -45,6 +45,7 @@ import type { TranscriptRow } from "../lib/chat/transcript/types"; import type { GatewayTranscriptRound } from "../lib/chatUi"; import type { SectionId } from "../pages/settings/types"; +import { ChatEmptyState } from "./chat/ChatEmptyState"; import { Check, CheckCircle2, @@ -54,7 +55,6 @@ import { FileText, Loader2, Pencil, - Settings, X, } from "./icons"; @@ -89,6 +89,7 @@ type GatewayTranscriptProps = { text: string, uploadedFiles: PendingUploadedFile[], ) => void; + onSuggestionSelect?: (text: string) => void; readOnly?: boolean; redactToolContent?: boolean; }; @@ -1339,10 +1340,10 @@ export function GatewayTranscript({ gitClient, onLoadUploadedImagePreview, onResendFromEdit, + onSuggestionSelect, readOnly = false, redactToolContent = false, }: GatewayTranscriptProps) { - const { t } = useLocale(); const transcriptListRef = useRef(null); const [transcriptScrollViewport, setTranscriptScrollViewport] = useState( null, @@ -1373,49 +1374,11 @@ export function GatewayTranscript({ return (
-
-
- -
- -

- {showNoModelsState ? t("chat.welcome") : t("chat.startChat")} -

- - {showNoModelsState ? ( - <> -

- {t("chat.noModelSelected")} -

-

- {t("chat.configureModel")} -

- {onOpenSettings ? ( - - ) : null} - - ) : ( -

- {t("chat.startChatDesc")} -

- )} -
+
); diff --git a/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx b/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx new file mode 100644 index 00000000..cea184d8 --- /dev/null +++ b/crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx @@ -0,0 +1,187 @@ +import { + type CSSProperties, + type PointerEvent as ReactPointerEvent, + useCallback, + useEffect, + useState, +} from "react"; + +import { FolderTree, Lightbulb, Settings, Wrench } from "@/components/icons"; +import { useLocale } from "@/i18n/LocaleContext"; +import type { SectionId } from "@/pages/settings/types"; + +type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "night"; + +const GREETING_KEYS: Record = { + morning: "chat.greetingMorning", + noon: "chat.greetingNoon", + afternoon: "chat.greetingAfternoon", + evening: "chat.greetingEvening", + night: "chat.greetingNight", +}; + +function resolveGreetingPeriod(hour: number): GreetingPeriod { + if (hour >= 5 && hour < 12) return "morning"; + if (hour >= 12 && hour < 14) return "noon"; + if (hour >= 14 && hour < 18) return "afternoon"; + if (hour >= 18 && hour < 23) return "evening"; + return "night"; +} + +function useGreetingPeriod() { + const [period, setPeriod] = useState(() => + resolveGreetingPeriod(new Date().getHours()), + ); + + useEffect(() => { + const timer = window.setInterval(() => { + setPeriod(resolveGreetingPeriod(new Date().getHours())); + }, 60_000); + return () => window.clearInterval(timer); + }, []); + + return period; +} + +const SUGGESTION_CARDS = [ + { + key: "explore", + icon: FolderTree, + accent: "199 89% 48%", + chipClassName: + "bg-sky-500/10 text-sky-600 group-hover:bg-sky-500/20 dark:bg-sky-400/10 dark:text-sky-400 dark:group-hover:bg-sky-400/20", + titleKey: "chat.suggestExploreTitle", + hintKey: "chat.suggestExploreHint", + promptKey: "chat.suggestExplorePrompt", + }, + { + key: "fix", + icon: Wrench, + accent: "38 92% 50%", + chipClassName: + "bg-amber-500/10 text-amber-600 group-hover:bg-amber-500/20 dark:bg-amber-400/10 dark:text-amber-400 dark:group-hover:bg-amber-400/20", + titleKey: "chat.suggestFixTitle", + hintKey: "chat.suggestFixHint", + promptKey: "chat.suggestFixPrompt", + }, + { + key: "ideate", + icon: Lightbulb, + accent: "160 84% 39%", + chipClassName: + "bg-emerald-500/10 text-emerald-600 group-hover:bg-emerald-500/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:group-hover:bg-emerald-400/20", + titleKey: "chat.suggestIdeateTitle", + hintKey: "chat.suggestIdeateHint", + promptKey: "chat.suggestIdeatePrompt", + }, +] as const; + +export type ChatEmptyStateProps = { + variant: "no-models" | "start-chat"; + onOpenSettings?: (section?: SectionId) => void; + onSuggestionSelect?: (text: string) => void; +}; + +export function ChatEmptyState({ + variant, + onOpenSettings, + onSuggestionSelect, +}: ChatEmptyStateProps) { + const { t } = useLocale(); + const period = useGreetingPeriod(); + + // Drives the accent spotlight that follows the cursor inside each card. + const handleCardPointerMove = useCallback((event: ReactPointerEvent) => { + const card = event.currentTarget; + const rect = card.getBoundingClientRect(); + card.style.setProperty("--spot-x", `${event.clientX - rect.left}px`); + card.style.setProperty("--spot-y", `${event.clientY - rect.top}px`); + }, []); + + return ( +
+
+ + + {variant === "no-models" ? ( + <> +
+ {t("chat.welcome")} +
+
+ {t("chat.noModelSelected")} +
+
+ {t("chat.configureModel")} +
+ {onOpenSettings ? ( + + ) : null} + + ) : ( + <> +
+ {t(GREETING_KEYS[period])} +
+
+
+ {onSuggestionSelect ? ( +
+ {SUGGESTION_CARDS.map((card, index) => ( + + ))} +
+ ) : null} + + )} +
+ ); +} diff --git a/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx b/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx index f281dc71..fe1a7a32 100644 --- a/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx +++ b/crates/agent-gateway/web/src/components/chat/ChatHistorySidebar.tsx @@ -1798,14 +1798,14 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi
+ ) : null} + + ) : ( + <> +
+ {t(GREETING_KEYS[period])} +
+
+
+ {onSuggestionSelect ? ( +
+ {SUGGESTION_CARDS.map((card, index) => ( + + ))} +
+ ) : null} + + )} +
+ ); +} diff --git a/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx b/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx index e5c2068e..0f882b83 100644 --- a/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx +++ b/crates/agent-gui/src/pages/chat/transcript/ChatTranscript.tsx @@ -9,11 +9,11 @@ import { } from "react"; import { createPortal } from "react-dom"; -import iconSimpleUrl from "../../../../src-tauri/icons/icon-simple.png"; -import { Copy, Settings } from "../../../components/icons"; +import { Copy } from "../../../components/icons"; import { ScrollArea } from "../../../components/ui/scroll-area"; import { useLocale } from "../../../i18n"; import { resolveScrollViewport } from "../utils/chatScrollViewport"; +import { ChatEmptyState } from "./ChatEmptyState"; import { TranscriptHistory } from "./TranscriptHistory"; import { TranscriptLiveState } from "./TranscriptLiveState"; import { HistorySwitchLoadingOverlay } from "./TranscriptLoadingStates"; @@ -48,8 +48,9 @@ export const ChatTranscript = memo(function ChatTranscript(props: ChatTranscript setCopiedMessageKey, onResendFromEdit, onOpenSettings, + onSuggestionSelect, } = props; - const { t, locale } = useLocale(); + const { locale } = useLocale(); const showNoModelsState = !hasModels; const showStartChatState = hasModels && historyItems.length === 0 && !isSending; const shouldReserveTranscriptBottomSpace = !(showNoModelsState || showStartChatState); @@ -155,58 +156,13 @@ export const ChatTranscript = memo(function ChatTranscript(props: ChatTranscript >
- {showNoModelsState ? ( + {showNoModelsState || showStartChatState ? (
-
-
- -
-

- {t("chat.welcome")} -

-

- {t("chat.noModelSelected")} -

-

- {t("chat.configureModel")} -

- -
-
- ) : showStartChatState ? ( -
-
-
- -
- -

- {t("chat.startChat")} -

- -

- {t("chat.startChatDesc")} -

-
+
) : null} diff --git a/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts b/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts index 32ea2711..c7dc66b4 100644 --- a/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts +++ b/crates/agent-gui/src/pages/chat/transcript/transcriptTypes.ts @@ -33,6 +33,7 @@ export type ChatTranscriptProps = { attachments: PendingUploadedFile[], ) => void; onOpenSettings: (section?: SectionId) => void; + onSuggestionSelect?: (text: string) => void; }; export type TranscriptHistoryProps = Pick<