Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/temp/chat_only_ui_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Chat-Only UI Plan

## Current Frontend Structure Summary

- `frontend/src/App.tsx` defines all React routes under `BrowserRouter basename="/huf"`.
- Most workbench pages are wrapped in `UnifiedLayout`, which mounts `AppSidebar` and an optional header.
- `AppSidebar` contains normal product navigation and filters items through `PermissionsContext` capabilities. The existing Chat nav item points to `/chat`.
- Existing `/chat` and `/chat/:chatId` routes use `UnifiedLayout hideHeader`, so the app sidebar is still present even though the normal header is hidden.
- Auth is centralized in `UserContext`; `ProtectedRoute` only checks logged-in state.
- Logout is exposed through `NavUser`, but that component is sidebar-specific.
- Existing mobile detection uses `useIsMobile`; current chat page has a mobile overlay conversation list.

## Existing Chat UI/API Summary

- `ChatPageV2` renders a conversation/sidebar experience with `ChatListing` and `ChatWindowV2`.
- `ChatWindowV2` renders `ChatWindowHeader` and `ChatMessageList`, and closes the app sidebar through `useSidebar`.
- `ChatMessageList` is the best reuse point: it loads existing messages, handles socket/tool updates, renders markdown/artifacts through `ChatMessage`, shows loading/error states, and renders `ChatInput`.
- `ChatInput` sends via `streamChatApi.sendMessage`; it supports new and existing conversations, streaming fallback, audio transcription, loading state, and conversation-created callbacks.
- `chatApi.ts` handles conversation and message reads plus feedback/title updates.
- `agentApi.getAgentModels` already filters chat-enabled agents with `allow_chat = 1` and `disabled = 0`.

## Exact Files To Change

- `frontend/src/App.tsx` - add lazy route for standalone chat-only UI.
- `frontend/src/pages/ChatOnlyPage.tsx` - new dedicated route page.
- `frontend/src/components/chat-only/ChatOnlyLayout.tsx` - standalone product-like full-height shell.
- `frontend/src/components/chat-only/ChatHeader.tsx` - Huf identity plus user/logout menu.
- `frontend/src/components/chat-only/ChatAgentSelector.tsx` - mobile-friendly chat-agent picker.
- `frontend/src/components/chat/ChatMessageList.tsx` - pass optional new-conversation path into `ChatInput`.
- `frontend/src/components/chat/ChatInput.tsx` - accept optional new-conversation path builder for route reuse.
- `frontend/src/services/agentApi.ts` - add `getChatAgents`.
- `docs/temp/chat_only_ui_tracker.md` - keep implementation tracker current.
- `docs/temp/chat_only_ui_plan.md` - this plan.

## Proposed Route

Use `/ui/chat` and `/ui/chat/:chatId`, which becomes `/huf/ui/chat` in the deployed app because the router basename is `/huf`.

Reason: `/chat` already exists as the workbench chat experience with conversation listing and app layout behavior. `/ui/chat` cleanly matches the user-preferred URL while avoiding disruption to existing routes.

## Component Plan

- `ChatOnlyPage` owns route params, selected agent, available-agent loading, and navigation.
- `ChatOnlyLayout` provides the standalone full-height shell without `UnifiedLayout` or sidebar.
- `ChatHeader` shows a compact Huf brand mark, current agent label when available, and a user menu with logout from `UserContext`.
- `ChatAgentSelector` shows no-agent, one-agent, and multi-agent selection states. One enabled chat agent is auto-selected by the page.
- Reuse `ChatMessageList` for message loading, markdown rendering, sending, typing/loading, and error display.

## Mobile Behavior Plan

- Use `h-[100svh]`/`min-h-0` flex layout so mobile browser chrome does not push the input away.
- Keep header compact and sticky at the top of the standalone shell.
- Let `ChatMessageList` own natural scroll and bottom input behavior.
- Use full-screen width on phones; constrain only on larger screens with a centered chat container.
- Agent selector uses large touch targets and avoids desktop side panels.

## Permission/Access Assumptions

- The chat-only route remains behind `ProtectedRoute` for authenticated users.
- Agents are exposed only if returned by Frappe through `getChatAgents`, filtered to `allow_chat = 1` and `disabled = 0`.
- If permissions hide all agents, the page shows "No chat access available".
- Existing backend permissions remain authoritative for conversation/message reads and sends.

## Implementation Checklist

- [x] Add route for chat-only UI.
- [x] Add standalone chat-only page/layout.
- [x] Reuse existing chat service/hooks where possible.
- [x] Add responsive CSS/Tailwind classes.
- [x] Add logo/logout in header.
- [x] Handle one-agent, multi-agent, and no-agent states.
- [x] Test desktop and mobile viewport.
- [x] Run build/lint/typecheck if available.

## Risks/Unknowns

- `ChatMessageList` currently reads the new-chat agent from the URL search param; `ChatOnlyPage` must keep `?agent=` in sync for new conversations.
- `ChatInput` has one hardcoded `/chat/new` path for model mismatch; this needs a small optional prop to preserve route isolation.
- Backend access may differ by role; local UI can filter only what the current session can list.
- Existing `UserContext` login redirect points back to `/huf`, not `/huf/ui/chat`; changing that globally is out of scope.

## Testing Checklist

- [x] Build passes.
- [ ] Existing app/sidebar routes still render through existing layout.
- [ ] `/huf/ui/chat` renders without sidebar.
- [ ] One chat-enabled agent opens directly.
- [ ] Multiple chat-enabled agents show a mobile-friendly selector.
- [ ] No chat-enabled agents show a clean empty state.
- [ ] Logout remains available.
- [x] Mobile message list scrolls and input remains reachable.

Note: browser viewport testing was skipped per user request after build verification.
82 changes: 82 additions & 0 deletions docs/temp/chat_only_ui_tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Chat-Only UI Tracker

## Objective

Build a dedicated standalone chat experience for users who only have access to conversational agents, reachable from a clean chat route without the normal admin/workbench sidebar. The page should reuse existing Huf chat APIs and auth/logout behavior, support one-agent, multi-agent, and no-agent states, and work well on mobile.

## Current Understanding

- Started from `origin/develop` on branch `feat_chat_ui`.
- Local `develop` had diverged from `origin/develop`, so the feature branch was created directly from `origin/develop` to use latest `tridz-dev/huf` code without rewriting local history.
- Routing lives in `frontend/src/App.tsx` under `BrowserRouter basename="/huf"`.
- Normal pages use `UnifiedLayout`, which always mounts `AppSidebar`; the existing `/chat` route uses `UnifiedLayout hideHeader`, so it still has app sidebar mechanics.
- Current chat page is `frontend/src/pages/ChatPageV2.tsx`. It renders `ChatListing` plus `ChatWindowV2`.
- `ChatWindowV2` calls `useSidebar().setOpen(false)`, so it depends on `SidebarProvider` from `UnifiedLayout`.
- Chat messages/input are mostly reusable through `ChatMessageList`, `ChatInput`, and the streaming service. `ChatMessageList` accepts an explicit `chatId` prop and uses the `agent` search param for new chats.
- Existing chat services are in `frontend/src/services/chatApi.ts`; message sending uses `frontend/src/services/streamChatApi.ts`.
- Existing allowed-chat agent selector is `AgentModelSelector`, backed by `getAgentModels` in `agentApi.ts`, which filters `allow_chat = 1` and `disabled = 0`.
- Auth/logout lives in `UserContext`; `NavUser` uses the same `logout` and `user` values but is coupled to sidebar UI.
- Permission context exposes `chat.use`, but route-level capability checks are not currently enforced by `ProtectedRoute`; sidebar visibility uses capabilities.
- Added route-level chat-only redirect guard: users whose only capability is `chat.use` are redirected from full app routes to `/ui/chat`, while `/ui/chat*` and `/view/*` remain accessible.
- Mobile pattern: `useIsMobile` is used in `ChatPageV2`; existing chat already prioritizes full-height flex layout and scrollable message list.

## Decisions

- Route: `/ui/chat` and `/ui/chat/:chatId` inside the `/huf` basename, yielding `/huf/ui/chat`.
- Layout approach: route-level standalone page outside `UnifiedLayout` so no app sidebar is mounted.
- Components to reuse: `ChatMessageList`, `ChatInput`, `ChatMessage`, markdown/artifact rendering, existing `streamChatApi`, existing `chatApi`, existing `UserContext`.
- Components to create: `ChatOnlyPage`, `ChatOnlyLayout`, `ChatHeader`, and `ChatAgentSelector`.
- API addition: add `getChatAgents` to `agentApi.ts` for a simple list of enabled chat agents using the same `allow_chat = 1` and `disabled = 0` criteria.
- Assumption: if the user can read an enabled `allow_chat` agent returned by Frappe permissions, it is valid to expose in chat-only selector.

## TODO Checklist

- [x] Switch to latest `tridz-dev/huf` code base safely.
- [x] Create `feat_chat_ui` branch from `origin/develop`.
- [x] Create tracker file.
- [x] Create planning document.
- [x] Inspect frontend pages, components, contexts, services, layout, navigation, chat, routing, auth/logout, and mobile patterns.
- [x] Update tracker with inspection notes.
- [x] Complete planning document before implementation.
- [x] Implement chat-only route.
- [x] Implement standalone chat-only page/layout/header/agent selector.
- [x] Reuse existing chat service/components where practical.
- [x] Handle one-agent, multi-agent, and no-agent states.
- [x] Verify desktop and mobile layout.
- [x] Run build/lint/typecheck where available.
- [ ] Commit, push, and open PR.
- [x] Add permission redirect behavior for chat-only users.
- [x] Update tracker and plan to final state.

## Scratchpad

- Existing `/chat` should remain unchanged for admin/workbench use.
- Existing `/chat` now redirects to `/ui/chat` only for users whose capability list is exactly `["chat.use"]`; broader users keep the desktop/full-app chat route.
- Need avoid using `ChatWindowV2` in standalone route because it requires `useSidebar`.
- `ChatMessageList` can be reused directly if standalone page controls selected/new agent.
- Added `getNewConversationPath` optional prop through `ChatMessageList` to `ChatInput` to avoid hardcoded `/chat/new` when used by `/ui/chat`.
- User said browser testing is not needed, so skipped desktop/mobile visual smoke test.

## Files Changed

- `docs/temp/chat_only_ui_tracker.md` - persistent task tracker, TODO list, and scratchpad.
- `docs/temp/chat_only_ui_plan.md` - temporary implementation plan.
- `frontend/src/App.tsx` - adds `/ui/chat` and `/ui/chat/:chatId` protected routes.
- `frontend/src/App.tsx` - also redirects chat-only users away from full app routes.
- `frontend/src/pages/ChatOnlyPage.tsx` - standalone chat-only page state and routing.
- `frontend/src/components/chat-only/ChatOnlyLayout.tsx` - standalone full-height shell without sidebar.
- `frontend/src/components/chat-only/ChatHeader.tsx` - Huf identity and user/logout menu.
- `frontend/src/components/chat-only/ChatAgentSelector.tsx` - mobile-friendly chat agent selection and empty states.
- `frontend/src/components/chat/ChatMessageList.tsx` - passes route-safe new conversation path to input.
- `frontend/src/components/chat/ChatInput.tsx` - accepts optional new conversation route builder.
- `frontend/src/services/agentApi.ts` - adds `getChatAgents` filtered to enabled chat agents.

## Testing Log

- `yarn typecheck` in `frontend` - initially failed because declared dependencies `media-chrome` and `prismjs` were missing from `node_modules`.
- `yarn build` in `frontend` - initially failed for the same missing dependencies.
- `yarn lint` in `frontend` - failed on existing repo-wide lint issues (hundreds of pre-existing `any`/unused-vars/hook warnings across unrelated files).
- `yarn install --frozen-lockfile` in `frontend` - completed; no yarn lockfile exists, dependencies were installed from `package.json`.
- `yarn build` in `frontend` - sandbox run reached Vite output but failed clearing `huf/public/frontend/assets` with `EPERM`.
- `yarn build` in `frontend` with elevated filesystem access - passed. Vite emitted existing large chunk warnings only.
- Browser/mobile smoke test - skipped per user request.
43 changes: 40 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { BrowserRouter, Navigate, Routes, Route, useLocation } from 'react-router-dom';
import { UserProvider } from './contexts/UserContext';
import { PermissionsProvider } from './contexts/PermissionsContext';
import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { AuthenticatingPage } from './components/AuthenticatingPage';
import { FlowProvider } from './contexts/FlowContext';
Expand Down Expand Up @@ -31,6 +31,7 @@ const FlowCanvasPageWrapper = lazy(() => import('./pages/FlowCanvasPageWrapper')
const DataPage = lazy(() => import('./pages/DataPage'));
const IntegrationsPageWrapper = lazy(() => import('./pages/IntegrationsPageWrapper'));
const ChatPage = lazy(() => import('./pages/ChatPageV2'));
const ChatOnlyPage = lazy(() => import('./pages/ChatOnlyPage'));
const Executions = lazy(() => import('./pages/Executions'));
const AgentRunDetailPage = lazy(() => import('./pages/AgentRunDetailPage'));
const McpDetailsPageWrapper = lazy(() => import('./pages/McpDetailsPageWrapper'));
Expand All @@ -51,6 +52,21 @@ import {
const UsersPage = lazy(() => import('./pages/UsersPage'));
const RolesPage = lazy(() => import('./pages/RolesPage'));

function ChatOnlyRedirectGuard() {
const location = useLocation();
const { capabilities, isLoading } = usePermissions();
const isChatOnlyUser =
capabilities.includes('chat.use') && capabilities.every((capability) => capability === 'chat.use');
const isAllowedChatOnlyPath =
location.pathname.startsWith('/ui/chat') || location.pathname.startsWith('/view/');

if (isLoading || !isChatOnlyUser || isAllowedChatOnlyPath) {
return null;
}

return <Navigate to="/ui/chat" replace />;
}

function App() {
useEffect(() => {
const connectionDescription =
Expand Down Expand Up @@ -131,6 +147,7 @@ function App() {
<BrowserRouter basename="/huf">
<UserProvider>
<PermissionsProvider>
<ChatOnlyRedirectGuard />
<Suspense fallback={<AuthenticatingPage />}>
<Routes>
<Route
Expand Down Expand Up @@ -307,6 +324,26 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/ui/chat"
element={
<ProtectedRoute>
<Suspense fallback={<PageLoader />}>
<ChatOnlyPage />
</Suspense>
</ProtectedRoute>
}
/>
<Route
path="/ui/chat/:chatId"
element={
<ProtectedRoute>
<Suspense fallback={<PageLoader />}>
<ChatOnlyPage />
</Suspense>
</ProtectedRoute>
}
/>
<Route
path="/executions"
element={
Expand Down Expand Up @@ -450,4 +487,4 @@ function App() {
);
}

export default App;
export default App;
125 changes: 125 additions & 0 deletions frontend/src/components/chat-only/ChatAgentSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Bot, MessageSquarePlus } from "lucide-react";
import type { ReactNode } from "react";
import ChatAvatar from "@/components/chat/ChatAvatar";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { DEFAULT_AGENT_COLOR } from "@/data/color";
import { getInitials } from "@/utils/getInitials";
import type { ChatAgentItem } from "@/services/agentApi";

interface ChatAgentSelectorProps {
agents: ChatAgentItem[];
loading: boolean;
error?: string | null;
onSelectAgent: (agentName: string) => void;
}

export function ChatAgentSelector({
agents,
loading,
error,
onSelectAgent,
}: ChatAgentSelectorProps) {
if (loading) {
return (
<div className="flex h-full items-center justify-center px-5">
<div className="w-full max-w-md space-y-3">
<Skeleton className="mx-auto size-12 rounded-full" />
<Skeleton className="mx-auto h-5 w-44" />
<Skeleton className="mx-auto h-4 w-64" />
<div className="space-y-2 pt-4">
<Skeleton className="h-14 w-full rounded-xl" />
<Skeleton className="h-14 w-full rounded-xl" />
<Skeleton className="h-14 w-full rounded-xl" />
</div>
</div>
</div>
);
}

if (error) {
return (
<CenteredState
icon={<Bot className="size-5" />}
title="Chat is unavailable"
description={error}
/>
);
}

if (agents.length === 0) {
return (
<CenteredState
icon={<Bot className="size-5" />}
title="No chat access available"
description="There are no enabled chat agents available for your account."
/>
);
}

return (
<div className="flex h-full items-center justify-center overflow-y-auto px-4 py-8">
<div className="w-full max-w-lg space-y-6">
<div className="space-y-2 text-center">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary">
<MessageSquarePlus className="size-5" />
</div>
<h1 className="text-xl font-semibold text-zinc-950">Choose an assistant</h1>
<p className="text-sm text-zinc-500">Start a focused chat with one of your available Huf agents.</p>
</div>

<div className="space-y-2">
{agents.map((agent) => (
<Button
key={agent.name}
type="button"
variant="outline"
className="h-auto w-full justify-start gap-3 rounded-xl px-4 py-3 text-left"
onClick={() => onSelectAgent(agent.name)}
>
<ChatAvatar variant="listing_ai" color={agent.agent_color || DEFAULT_AGENT_COLOR}>
{getInitials(agent.agent_name || agent.name)}
</ChatAvatar>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold text-zinc-900">
{agent.agent_name || agent.name}
</span>
{agent.description ? (
<span className="block truncate text-xs font-normal text-zinc-500">
{agent.description}
</span>
) : (
<span className="block truncate text-xs font-normal text-zinc-500">
{agent.model || "Chat agent"}
</span>
)}
</span>
</Button>
))}
</div>
</div>
</div>
);
}

function CenteredState({
icon,
title,
description,
}: {
icon: ReactNode;
title: string;
description: string;
}) {
return (
<div className="flex h-full items-center justify-center px-5 text-center">
<div className="max-w-sm space-y-3">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-zinc-100 text-zinc-500">
{icon}
</div>
<h1 className="text-lg font-semibold text-zinc-950">{title}</h1>
<p className="text-sm leading-6 text-zinc-500">{description}</p>
</div>
</div>
);
}
Loading