feat(ai): AI reading assistant phase 2.1 (minor UI/UX updates) (#3059)

* refactor(ai): remove chat icons from conversation list items

* feat(ai): add scroll-to-bottom button with animations and dynamic spacer

* fix(ai): align connection status icon with text in settings

* feat(ai): scroll to bottom on chat panel open and improve scroll behavior

* feat(ai): add loading overlay with blur transition for chat history

* feat(ai): pass loading state through component chain for history loading
This commit is contained in:
Mohammed Efaz
2026-01-24 22:57:00 +06:00
committed by GitHub
parent 42fee90a27
commit 8bea05478a
4 changed files with 218 additions and 47 deletions
@@ -84,7 +84,12 @@ const AIAssistantChat = ({
currentPage: number;
onResetIndex: () => void;
}) => {
const { activeConversationId, messages: storedMessages, addMessage } = useAIChatStore();
const {
activeConversationId,
messages: storedMessages,
addMessage,
isLoadingHistory,
} = useAIChatStore();
// use a ref to keep up-to-date options without triggering re-renders of the runtime
const optionsRef = useRef({
@@ -153,41 +158,55 @@ const AIAssistantChat = ({
adapter={adapter}
historyAdapter={historyAdapter}
onResetIndex={onResetIndex}
isLoadingHistory={isLoadingHistory}
hasActiveConversation={!!activeConversationId}
/>
);
};
// separate component to ensure useLocalRuntime is always called with a valid adapter
const AIAssistantWithRuntime = ({
adapter,
historyAdapter,
onResetIndex,
isLoadingHistory,
hasActiveConversation,
}: {
adapter: NonNullable<ReturnType<typeof createTauriAdapter>>;
historyAdapter?: ThreadHistoryAdapter;
onResetIndex: () => void;
isLoadingHistory: boolean;
hasActiveConversation: boolean;
}) => {
const runtime = useLocalRuntime(adapter, {
adapters: historyAdapter ? { history: historyAdapter } : undefined,
});
// ensure runtime is available before providing it
if (!runtime) return null;
return (
<AssistantRuntimeProvider runtime={runtime}>
<ThreadWrapper onResetIndex={onResetIndex} />
<ThreadWrapper
onResetIndex={onResetIndex}
isLoadingHistory={isLoadingHistory}
hasActiveConversation={hasActiveConversation}
/>
</AssistantRuntimeProvider>
);
};
// inner component that uses useAssistantRuntime (must be inside provider)
const ThreadWrapper = ({ onResetIndex }: { onResetIndex: () => void }) => {
const ThreadWrapper = ({
onResetIndex,
isLoadingHistory,
hasActiveConversation,
}: {
onResetIndex: () => void;
isLoadingHistory: boolean;
hasActiveConversation: boolean;
}) => {
const [sources, setSources] = useState(getLastSources());
const assistantRuntime = useAssistantRuntime();
const { setActiveConversation } = useAIChatStore();
// poll for sources updates (adapter stores them)
useEffect(() => {
const interval = setInterval(() => {
setSources(getLastSources());
@@ -202,7 +221,15 @@ const ThreadWrapper = ({ onResetIndex }: { onResetIndex: () => void }) => {
assistantRuntime.switchToNewThread();
}, [assistantRuntime, setActiveConversation]);
return <Thread sources={sources} onClear={handleClear} onResetIndex={onResetIndex} />;
return (
<Thread
sources={sources}
onClear={handleClear}
onResetIndex={onResetIndex}
isLoadingHistory={isLoadingHistory}
hasActiveConversation={hasActiveConversation}
/>
);
};
const AIAssistant = ({ bookKey }: AIAssistantProps) => {
@@ -153,8 +153,6 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({ bookKey }) => {
}
}}
>
<LuMessageSquare className='text-base-content/40 mt-0.5 size-4 flex-shrink-0' />
<div className='min-w-0 flex-1'>
{editingId === conversation.id ? (
<div