Skip to content
Merged
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
113 changes: 45 additions & 68 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,52 @@
import { createOpenAI } from "@ai-sdk/openai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models";
import { buildSystemMessage } from "@/lib/ai/prompt";

// Allow streaming responses up to 30 seconds
// 流式响应最长30秒
export const maxDuration = 30;

export async function POST(req: Request) {
const {
messages,
system,
pageContext,
provider,
apiKey,
}: {
messages: UIMessage[];
system?: string; // System message forwarded from AssistantChatTransport
tools?: unknown; // Frontend tools forwarded from AssistantChatTransport
pageContext?: {
title?: string;
description?: string;
content?: string;
slug?: string;
};
provider?: "openai" | "gemini";
apiKey?: string;
} = await req.json();

// Check if API key is provided
if (!apiKey || apiKey.trim() === "") {
return Response.json(
{
error:
"API key is required. Please configure your API key in the settings.",
},
{ status: 400 },
);
}
interface ChatRequest {
messages: UIMessage[];
system?: string;
tools?: unknown;
pageContext?: {
title?: string;
description?: string;
content?: string;
slug?: string;
};
provider?: AIProvider;
apiKey?: string;
}

export async function POST(req: Request) {
try {
// Build system message with page context
let systemMessage =
system ||
`You are a helpful AI assistant for a documentation website.
You can help users understand the documentation, answer questions about the content,
and provide guidance on the topics covered in the docs. Be concise and helpful.`;
const {
messages,
system,
pageContext,
provider = "intern", // 默认使用书生模型
apiKey,
}: ChatRequest = await req.json();

// Add current page context if available
if (pageContext?.content) {
systemMessage += `\n\n--- CURRENT PAGE CONTEXT ---\n`;
if (pageContext.title) {
systemMessage += `Page Title: ${pageContext.title}\n`;
}
if (pageContext.description) {
systemMessage += `Page Description: ${pageContext.description}\n`;
}
if (pageContext.slug) {
systemMessage += `Page URL: /docs/${pageContext.slug}\n`;
}
systemMessage += `Page Content:\n${pageContext.content}`;
systemMessage += `\n--- END OF CONTEXT ---\n\nWhen users ask about "this page", "current page", or refer to the content they're reading, use the above context to provide accurate answers. You can summarize, explain, or answer specific questions about the current page content.`;
// 对指定Provider验证key是否存在
if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) {
return Response.json(
{
error:
"API key is required. Please configure your API key in the settings.",
},
{ status: 400 },
);
}

// Select model based on provider
let model;
if (provider === "gemini") {
const customGoogle = createGoogleGenerativeAI({
apiKey: apiKey,
});
model = customGoogle("models/gemini-2.0-flash");
} else {
// Default to OpenAI
const customOpenAI = createOpenAI({
apiKey: apiKey,
});
model = customOpenAI("gpt-4.1-nano");
}
// 构建系统消息,包含页面上下文
const systemMessage = buildSystemMessage(system, pageContext);

// 根据Provider获取 AI 模型实例
const model = getModel(provider, apiKey);

// 生成流式响应
const result = streamText({
model: model,
system: systemMessage,
Expand All @@ -85,6 +56,12 @@ export async function POST(req: Request) {
return result.toUIMessageStreamResponse();
} catch (error) {
console.error("Chat API error:", error);

// 处理特定模型创建错误
if (error instanceof Error && error.message.includes("API key")) {
return Response.json({ error: error.message }, { status: 400 });
}

return Response.json(
{ error: "Failed to process chat request" },
{ status: 500 },
Expand Down
29 changes: 19 additions & 10 deletions app/components/DocsAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
const currentApiKey =
currentProvider === "openai"
? openaiApiKeyRef.current
: geminiApiKeyRef.current;
: currentProvider === "gemini"
? geminiApiKeyRef.current
: ""; // intern provider doesn't need API key

console.log("[DocsAssistant] useChat body function called with:", {
provider: currentProvider,
Expand Down Expand Up @@ -118,9 +120,14 @@ interface AssistantErrorState {

function deriveAssistantError(
err: unknown,
provider: "openai" | "gemini",
provider: "openai" | "gemini" | "intern",
): AssistantErrorState {
const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI";
const providerLabel =
provider === "gemini"
? "Google Gemini"
: provider === "intern"
? "Intern-AI"
: "OpenAI";
const fallback: AssistantErrorState = {
message:
"The assistant couldn't complete that request. Please try again later.",
Expand Down Expand Up @@ -176,14 +183,16 @@ function deriveAssistantError(

let showSettingsCTA = false;

// For intern provider, don't show settings CTA for API key related errors
if (
statusCode === 400 ||
statusCode === 401 ||
statusCode === 403 ||
normalized.includes("api key") ||
normalized.includes("apikey") ||
normalized.includes("missing key") ||
normalized.includes("unauthorized")
provider !== "intern" &&
(statusCode === 400 ||
statusCode === 401 ||
statusCode === 403 ||
normalized.includes("api key") ||
normalized.includes("apikey") ||
normalized.includes("missing key") ||
normalized.includes("unauthorized"))
) {
showSettingsCTA = true;
}
Expand Down
15 changes: 14 additions & 1 deletion app/components/assistant-ui/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ export const SettingsDialog = ({
<RadioGroup
value={provider}
onValueChange={(value) =>
setProvider(value as "openai" | "gemini")
setProvider(value as "openai" | "gemini" | "intern")
}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="intern" id="intern" />
<Label htmlFor="intern">InternS1 (Free)</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="openai" id="openai" />
<Label htmlFor="openai">OpenAI</Label>
Expand Down Expand Up @@ -90,6 +94,15 @@ export const SettingsDialog = ({
/>
</div>
)}

{provider === "intern" && (
<div className="space-y-2">
<div className="text-sm text-muted-foreground">
感谢上海AILab的书生大模型对本项目的算力支持,Intern-AI
模型已预配置,无需提供 API Key。
</div>
</div>
)}
</div>

<DialogFooter>
Expand Down
6 changes: 3 additions & 3 deletions app/components/assistant-ui/assistant-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const AssistantModal: FC<AssistantModalProps> = ({
}) => {
return (
<AssistantModalPrimitive.Root>
<AssistantModalPrimitive.Anchor className="aui-root aui-modal-anchor fixed right-4 bottom-4 size-11">
<AssistantModalPrimitive.Anchor className="aui-root aui-modal-anchor fixed right-4 bottom-4 size-14">
<AssistantModalPrimitive.Trigger asChild>
<AssistantModalButton />
</AssistantModalPrimitive.Trigger>
Expand Down Expand Up @@ -59,12 +59,12 @@ const AssistantModalButton = forwardRef<
>
<BotIcon
data-state={state}
className="aui-modal-button-closed-icon absolute size-6 transition-all data-[state=closed]:scale-100 data-[state=closed]:rotate-0 data-[state=open]:scale-0 data-[state=open]:rotate-90"
className="aui-modal-button-closed-icon absolute !size-7 transition-all data-[state=closed]:scale-100 data-[state=closed]:rotate-0 data-[state=open]:scale-0 data-[state=open]:rotate-90"
/>

<ChevronDownIcon
data-state={state}
className="aui-modal-button-open-icon absolute size-6 transition-all data-[state=closed]:scale-0 data-[state=closed]:-rotate-90 data-[state=open]:scale-100 data-[state=open]:rotate-0"
className="aui-modal-button-open-icon absolute !size-7 transition-all data-[state=closed]:scale-0 data-[state=closed]:-rotate-90 data-[state=open]:scale-100 data-[state=open]:rotate-0"
/>
<span className="aui-sr-only sr-only">{tooltip}</span>
</TooltipIconButton>
Expand Down
18 changes: 14 additions & 4 deletions app/components/assistant-ui/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,27 @@ const Composer: FC<ComposerProps> = ({
onClearError,
}) => {
const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings();
const activeKey = provider === "openai" ? openaiApiKey : geminiApiKey;
const hasActiveKey = activeKey.trim().length > 0;
const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI";
const activeKey =
provider === "openai"
? openaiApiKey
: provider === "gemini"
? geminiApiKey
: "";
const hasActiveKey = provider === "intern" || activeKey.trim().length > 0;
const providerLabel =
provider === "gemini"
? "Google Gemini"
: provider === "intern"
? "Intern-AI"
: "OpenAI";

const handleOpenSettings = useCallback(() => {
onClearError?.();
onOpenChange(true);
}, [onClearError, onOpenChange]);

return (
<div className="aui-composer-wrapper sticky bottom-0 mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 overflow-visible rounded-t-3xl pb-4 md:pb-6">
<div className="aui-composer-wrapper sticky bottom-0 mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 overflow-visible rounded-t-3xl bg-white pb-4 md:pb-6">
<ThreadScrollToBottom />
<ThreadPrimitive.Empty>
<ThreadWelcomeSuggestions />
Expand Down
9 changes: 7 additions & 2 deletions app/hooks/useAssistantSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "react";
import type { ReactNode } from "react";

type Provider = "openai" | "gemini";
type Provider = "openai" | "gemini" | "intern";

interface AssistantSettingsState {
provider: Provider;
Expand Down Expand Up @@ -45,7 +45,12 @@ const parseStoredSettings = (raw: string | null): AssistantSettingsState => {
try {
const parsed = JSON.parse(raw) as Partial<AssistantSettingsState>;
return {
provider: parsed.provider === "gemini" ? "gemini" : "openai",
provider:
parsed.provider === "gemini"
? "gemini"
: parsed.provider === "intern"
? "intern"
: "openai",
openaiApiKey:
typeof parsed.openaiApiKey === "string" ? parsed.openaiApiKey : "",
geminiApiKey:
Expand Down
43 changes: 43 additions & 0 deletions lib/ai/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createOpenAIModel } from "./providers/openai";
import { createGeminiModel } from "./providers/gemini";
import { createInternModel } from "./providers/intern";

export type AIProvider = "openai" | "gemini" | "intern";

/**
* Model工厂 用于返回对应的 AI 模型实例
* @param provider - 要用的provider
* @param apiKey - API key (intern provider不需要用户提供 API key)
* @returns 配置好的 AI 模型实例
*/
export function getModel(provider: AIProvider, apiKey?: string) {
switch (provider) {
case "openai":
if (!apiKey || apiKey.trim() === "") {
throw new Error("OpenAI API key is required");
}
return createOpenAIModel(apiKey);

case "gemini":
if (!apiKey || apiKey.trim() === "") {
throw new Error("Gemini API key is required");
}
return createGeminiModel(apiKey);

case "intern":
// Intern 书生模型不需要用户提供 API key
return createInternModel();

default:
throw new Error(`Unsupported AI provider: ${provider}`);
}
}

/**
* 检查指定的提供者是否需要用户提供 API key
* @param provider - 要检查的provider
* @returns 如果需要 API key,返回 true,否则返回 false
*/
export function requiresApiKey(provider: AIProvider): boolean {
return provider !== "intern";
}
46 changes: 46 additions & 0 deletions lib/ai/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
interface PageContext {
title?: string;
description?: string;
content?: string;
slug?: string;
}

/**
* 构建系统消息,包含页面上下文
* @param customSystem - 自定义系统消息 (可选)
* @param pageContext - 当前页面上下文 (可选)
* @returns 完整的系统消息字符串
*/
export function buildSystemMessage(
customSystem?: string,
pageContext?: PageContext,
): string {
// 默认系统消息
let systemMessage =
customSystem ||
`You are a helpful AI assistant for a documentation website.
Always respond in the same language as the user's question: if the user asks in 中文, answer in 中文; if the user asks in English, answer in English.
You can help users understand the documentation, answer questions about the content, and provide guidance on the topics covered in the docs. Be concise and helpful.`;

// 如果当前页面上下文可用,则添加到系统消息中
if (pageContext?.content) {
systemMessage += `\n\n--- CURRENT PAGE CONTEXT ---\n`;

if (pageContext.title) {
systemMessage += `Page Title: ${pageContext.title}\n`;
}

if (pageContext.description) {
systemMessage += `Page Description: ${pageContext.description}\n`;
}

if (pageContext.slug) {
systemMessage += `Page URL: /docs/${pageContext.slug}\n`;
}

systemMessage += `Page Content:\n${pageContext.content}`;
systemMessage += `\n--- END OF CONTEXT ---\n\nWhen users ask about "this page", "current page", or refer to the content they're reading, use the above context to provide accurate answers. You can summarize, explain, or answer specific questions about the current page content.`;
}

return systemMessage;
}
Loading