AI: improve defaults

This commit is contained in:
Fuma Nama
2026-03-24 18:57:24 -07:00
committed by zeekay
parent f85fcde24a
commit 55a5cb25c9
3 changed files with 73 additions and 20 deletions
+27 -9
View File
@@ -15,18 +15,19 @@ import { Loader2, MessageCircleIcon, RefreshCw, Send, X } from 'lucide-react';
import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-base-ui/components/ui/button';
import Link from '@hanzo/docs-core/link';
import { type UIMessage, useChat, type UseChatHelpers } from '@ai-sdk/react';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import type { ProvideLinksToolSchema } from '@/lib/chat/inkeep-qa-schema';
import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { chatEndpoint, publishableKey } from '@/lib/hanzo/client';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import type { InkeepUIMessage } from '@/lib/inkeep/server';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<UIMessage>;
chat: UseChatHelpers<InkeepUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -116,11 +117,27 @@ export function AISearchInput(props: ComponentProps<'form'>) {
const isLoading = status === 'streaming' || status === 'submitted';
const onStart = (e?: SyntheticEvent) => {
e?.preventDefault();
void sendMessage({ text: input });
setInput('');
};
const message = input.trim();
if (message.length === 0) return;
localStorage.setItem(StorageKeyInput, input);
void sendMessage({
role: 'user',
parts: [
{
type: 'data-client',
data: {
location: location.href,
},
},
{
type: 'text',
text: message,
},
],
});
setInput('');
localStorage.removeItem(StorageKeyInput);
};
useEffect(() => {
if (isLoading) document.getElementById('nd-ai-input')?.focus();
@@ -136,6 +153,7 @@ export function AISearchInput(props: ComponentProps<'form'>) {
disabled={status === 'streaming' || status === 'submitted'}
onChange={(e) => {
setInput(e.target.value);
localStorage.setItem(StorageKeyInput, e.target.value);
}}
onKeyDown={(event) => {
if (!event.shiftKey && event.key === 'Enter') {
@@ -243,7 +261,7 @@ const roleName: Record<string, string> = {
assistant: 'hanzo-bot',
};
function Message({ message, ...props }: { message: UIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: InkeepUIMessage } & ComponentProps<'div'>) {
let markdown = '';
let links: z.infer<typeof ProvideLinksToolSchema>['links'] = [];
@@ -297,7 +315,7 @@ const systemPrompt =
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat({
const chat = useChat<InkeepUIMessage>({
id: 'search',
initialMessages: [{ id: 'system', role: 'system' as const, content: systemPrompt, parts: [] }],
transport: new DefaultChatTransport({
@@ -323,7 +341,7 @@ export function AISearchTrigger({
data-state={open ? 'open' : 'closed'}
className={cn(
position === 'float' && [
'fixed bottom-4 gap-3 w-24 end-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]',
'fixed bottom-4 gap-3 w-24 inset-e-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]',
open && 'translate-y-10 opacity-0',
],
className,
+24 -7
View File
@@ -14,16 +14,16 @@ import {
import { Loader2, MessageCircleIcon, RefreshCw, SearchIcon, Send, X } from 'lucide-react';
import { cn } from '@/lib/cn';
import { buttonVariants } from '@hanzo/docs-ui/components/ui/button';
import { type UIMessage, useChat, type UseChatHelpers } from '@ai-sdk/react';
import { useChat, type UseChatHelpers } from '@ai-sdk/react';
import { DefaultChatTransport, type Tool, type UIToolInvocation } from 'ai';
import { Markdown } from '../markdown';
import { Presence } from '@radix-ui/react-presence';
import type { SearchTool } from '@/lib/openrouter/server';
import type { ChatUIMessage, SearchTool } from '@/lib/openrouter/server';
const Context = createContext<{
open: boolean;
setOpen: (open: boolean) => void;
chat: UseChatHelpers<UIMessage>;
chat: UseChatHelpers<ChatUIMessage>;
} | null>(null);
export function AISearchPanelHeader({ className, ...props }: ComponentProps<'div'>) {
@@ -110,7 +110,24 @@ export function AISearchInput(props: ComponentProps<'form'>) {
const isLoading = status === 'streaming' || status === 'submitted';
const onStart = (e?: SyntheticEvent) => {
e?.preventDefault();
void sendMessage({ text: input });
const message = input.trim();
if (message.length === 0) return;
void sendMessage({
role: 'user',
parts: [
{
type: 'data-client',
data: {
location: location.href,
},
},
{
type: 'text',
text: message,
},
],
});
setInput('');
localStorage.removeItem(StorageKeyInput);
};
@@ -237,7 +254,7 @@ const roleName: Record<string, string> = {
assistant: 'fumadocs',
};
function Message({ message, ...props }: { message: UIMessage } & ComponentProps<'div'>) {
function Message({ message, ...props }: { message: ChatUIMessage } & ComponentProps<'div'>) {
let markdown = '';
const searchCalls: UIToolInvocation<SearchTool>[] = [];
@@ -291,7 +308,7 @@ function Message({ message, ...props }: { message: UIMessage } & ComponentProps<
export function AISearch({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const chat = useChat({
const chat = useChat<ChatUIMessage>({
id: 'search',
transport: new DefaultChatTransport({
api: '/api/chat',
@@ -315,7 +332,7 @@ export function AISearchTrigger({
data-state={open ? 'open' : 'closed'}
className={cn(
position === 'float' && [
'fixed bottom-4 gap-3 w-24 end-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]',
'fixed bottom-4 gap-3 w-24 inset-e-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] shadow-lg z-20 transition-[translate,opacity]',
open && 'translate-y-10 opacity-0',
],
className,
+22 -4
View File
@@ -11,6 +11,15 @@ interface CustomDocument extends DocumentData {
content: string;
}
export type ChatUIMessage = UIMessage<
never,
{
client: {
location: string;
};
}
>;
const searchServer = createSearchServer();
async function createSearchServer() {
@@ -55,6 +64,7 @@ const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
/** System prompt, you can update it to provide more specific information */
const systemPrompt = [
'You are an AI assistant for a documentation site.',
'Use the `search` tool to retrieve relevant docs context before answering when needed.',
@@ -63,7 +73,7 @@ const systemPrompt = [
].join('\n');
export async function POST(req: Request) {
const reqJson: { messages?: UIMessage[] } = await req.json();
const reqJson = await req.json();
const result = streamText({
model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'),
@@ -73,7 +83,15 @@ export async function POST(req: Request) {
},
messages: [
{ role: 'system', content: systemPrompt },
...(await convertToModelMessages(reqJson.messages ?? [])),
...(await convertToModelMessages<ChatUIMessage>(reqJson.messages ?? [], {
convertDataPart(part) {
if (part.type === 'data-client')
return {
type: 'text',
text: `[Client Context: ${JSON.stringify(part.data)}]`,
};
},
})),
],
toolChoice: 'auto',
});
@@ -81,6 +99,8 @@ export async function POST(req: Request) {
return result.toUIMessageStreamResponse();
}
export type SearchTool = typeof searchTool;
const searchTool = tool({
description: 'Search the docs content and return raw JSON results.',
inputSchema: z.object({
@@ -92,5 +112,3 @@ const searchTool = tool({
return await search.searchAsync(query, { limit, merge: true, enrich: true });
},
});
export type SearchTool = typeof searchTool;