research-agent-ui
v0.1.261
Published
The QwkSearch app UI: conversation window, article reader, search config, file uploads, chat history, and the app shell — exported both with and without the REASON editor and its sidebar
Maintainers
Readme
research-agent-ui
The QwkSearch app UI: conversation window, article reader, search config, file uploads, chat history — plus the app shell (providers, app dock, cookie banner, the research/docs view switch) that assembles them into a whole app. Includes the shadcn primitives and icons the components depend on, so it can be dropped into a Next.js app with a single dependency.

Two entry points: with and without the editor
The package ships the same app twice, and the only difference is whether the REASON document editor and its file sidebar come along:
| Import from | You get | Extra dependencies |
| --- | --- | --- |
| research-agent-ui | Chat, search, article reader, app shell | none |
| research-agent-ui/workspace | All of the above plus the REASON editor, document tree, and file sidebar | react-reason-editor, react-reason-editor-sidebar |
research-agent-ui/workspace re-exports everything the root entry does, so a
host that wants documents imports from that one path rather than mixing the
two. Going the other way, the root entry's import graph never reaches
react-reason-editor — the editor's (large) dependency tree stays out of a
chat-only consumer's bundle entirely. test/entryBoundaries.test.ts enforces
that in both directions.
The two editor packages are declared as optional peer dependencies:
installing research-agent-ui on its own is enough for the chat-only build,
and package managers will not warn about the missing peers.
Spotlight search
QwkSearchProviders mounts a macOS-Spotlight-style command palette over the
whole app. Ctrl Space opens it (Cmd-Space belongs to
macOS), or call openSpotlight() from your own chrome. Typing searches past
chats, app pages, settings sections and actions at once, with "ask the research
agent this" pinned to the top; a leading letter scopes the search to one source
— c chats, t pages, s settings, a actions, w ask — and
Tab cycles between them.
import { QwkSearchProviders, openSpotlight } from 'research-agent-ui';
<QwkSearchProviders authClient={authClient} showSpotlight> {/* the default */}
<YourApp />
</QwkSearchProviders>;
// …and from a button somewhere in your own chrome:
<button onClick={() => openSpotlight()}>Search everything</button>;Rows that open a chat or a settings section go through onOpenChat /
onOpenSettings first, so a host rendering chats inline handles them without
navigating; returning anything but true falls back to /c/<id> and
/settings/<section>.
Usage
The whole app in one component
// Chat only — no editor, no sidebar.
import { QwkSearchApp } from 'research-agent-ui';
export default function Page() {
return (
<QwkSearchApp
authClient={myAuthClient}
config={{ appName: 'MyApp', footerLinks: myLinks }}
/>
);
}// The same app, with documents.
import { QwkSearchWorkspaceApp } from 'research-agent-ui/workspace';
export default function Page() {
return (
<QwkSearchWorkspaceApp
authClient={myAuthClient}
config={{ appName: 'MyApp', footerLinks: myLinks }}
/>
);
}QwkSearchProviders is the same shell without a page inside it, for hosts that
render their own routes within the app chrome. It accepts a ChromeProvider to
mount app-owned context (a settings modal, say) inside the stack, and
showDock / showCookieConsent / showToaster to opt out of individual
pieces.
Composing the pieces yourself
import {
ChatProvider,
SessionProvider,
ExtractPanelProvider,
ChatWindow,
configureResearchAgentUI,
} from 'research-agent-ui';
configureResearchAgentUI({
appName: 'MyApp',
getAutoMediaSearch: () => true,
// ...see ResearchAgentUIConfig for the full list of overridable values
});
function App() {
return (
<SessionProvider authClient={myAuthClient}>
<ExtractPanelProvider>
<ChatProvider>
<ChatWindow />
</ChatProvider>
</ExtractPanelProvider>
</SessionProvider>
);
}Storybook
Individual UI pieces can be browsed in isolation with Storybook.
Stories render against mock data only (see src/stories/mocks.ts) — no API,
auth session, or chat backend is required, so you can develop and review the
chat components (message header with timestamp/copy/edit actions, search
progress, follow-up suggestions, file & pasted-content cards) on their own.
# from packages/research-agent-ui
bun run storybook # dev server on http://localhost:6006
bun run build-storybook # static build → storybook-static/A light/dark toggle in the toolbar switches between the two token palettes
(mirrored from the host app's globals.css in .storybook/preview.css). Add
new stories next to their component as *.stories.tsx.
API Routes (research-agent-ui/api)
All 25 Next.js route handlers are exported from the research-agent-ui/api
subpath as factory functions. Each factory accepts a deps object that
injects your app's database, auth helpers, and other services, so the same
handler logic works in any Next.js project without hard-coding any imports.
How it works
The route logic lives in packages/research-agent-ui/src/api/handlers/.
Your app's app/api/agent/*/route.ts files become thin wrappers that call
the factory and re-export the HTTP method handlers.
Step 1 — install / workspace link
If you are in this monorepo, research-agent-ui is already linked via the
workspace:* protocol. For an external project, install the published
package:
npm install research-agent-ui
# or
bun add research-agent-uiStep 2 — create your route files
For each API path, create a route.ts that calls the matching factory and
passes in your app's dependencies. Every factory is named
create<RouteName>Handler and is exported from research-agent-ui/api.
Example: app/api/agent/chats/route.ts
import { createChatsHandler } from "research-agent-ui/api";
import { getDB } from "@/lib/database";
import { chats, messages } from "@/lib/database/schema";
import { requireUserId } from "@/lib/auth/session";
const handler = createChatsHandler({
getDB,
requireUserId,
schema: { chats, messages },
});
export const { GET, DELETE } = handler;Example: app/api/agent/article-followups/route.ts
import { createArticleFollowupsHandler } from "research-agent-ui/api";
import { getUserId } from "@/lib/auth/session";
import { getDB } from "@/lib/database";
import { user as userSchema } from "@/lib/database/schema";
import { getEnv } from "@/lib/env";
export const POST = createArticleFollowupsHandler({
getUserId,
requireUserId: async () => {
const id = await getUserId();
if (!id) throw new Error("Unauthorized");
return id;
},
getDB,
userSchema,
getEnv,
});All available factories and their dep shapes
| Factory | File | Required deps |
|---|---|---|
| createArticleFollowupsHandler | article-followups | getUserId, requireUserId, getDB, userSchema, getEnv |
| createArticleQAHandler | article-qa | getUserId, requireUserId, getDB, userSchema, getEnv |
| createChatsHandler | chats | getDB, requireUserId, schema.chats, schema.messages |
| createChatByIdHandler | chats/[id] | getDB, requireUserId, schema.chats, schema.messages |
| createChatsSearchHandler | chats/search | getDB, requireUserId, schema.chats, schema.messages |
| createChatsShareHandler | chats/share | getDB, requireUserId, schema.chats, schema.messages |
| createMessagesHandler | messages | getDB, requireUserId, messagesSchema |
| createProvidersHandler | providers | getSession |
| createProviderByIdHandler | providers/[id] | (none) |
| createProviderModelsHandler | providers/[id]/models | (none) |
| createMCPServersHandler | mcpservers | configManager, getConfiguredMCPServers |
| createMCPServerByIdHandler | mcpservers/[id] | configManager, getConfiguredMCPServers |
| createMCPServerToggleHandler | mcpservers/[id]/toggle | configManager, getConfiguredMCPServers |
| createSearchHandler | search | searxngDomain? (default: https://search.qwksearch.com) |
| createDiscoverHandler | discover | (none) |
| createAutocompleteHandler | autocomplete | (none) |
| createSuggestionsHandler | suggestions | (none) |
| createAgentsHandler | agents | getUserId, requireUserId, getDB, userSchema, getEnv |
| createRewriteHandler | rewrite | getEnv, generateText, createGroq |
| createVoiceHandler | voice | getUserId, checkTTSRateLimit, generateSpeech |
| createTranscriptHandler | transcript | getCloudflareContext |
| createTestModelsHandler | test-models | (none) |
| createValidateOpenRouterHandler | validate-openrouter | validateOpenRouterModels |
Dep type definitions
All dep interfaces are exported from research-agent-ui/api:
import type {
ArticleDeps,
ChatsDeps,
MessagesDeps,
ProvidersDeps,
MCPServersDeps,
SearchDeps,
VoiceDeps,
TranscriptDeps,
RewriteDeps,
ValidateOpenRouterDeps,
AgentsDeps,
} from "research-agent-ui/api";The chat route
POST /api/agent/chat is not migrated into this package because it delegates
to a full handleChatRequest orchestrator that is app-specific (streaming,
search integration, database writes). Keep it directly in your app:
// app/api/agent/chat/route.ts
import { handleChatRequest } from "@/lib/chat";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const POST = handleChatRequest;Configuration
configureResearchAgentUI overrides app-specific values (branding strings,
the Google API key used by the Drive picker, and the auto-media-search
toggle) that would otherwise couple this package to a specific app. See
ResearchAgentUIConfig in src/config.ts for the full list.
