Skip to content

Commit 9e69f37

Browse files
authored
Merge pull request #130 from Crokily/addInternS1
feat: 对AI助手增加了书生大模型的支持,并解耦了AI模块的代码便于后续复用
2 parents e813da5 + 1dbb235 commit 9e69f37

File tree

13 files changed

+273
-88
lines changed

13 files changed

+273
-88
lines changed

app/api/chat/route.ts

Lines changed: 45 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,52 @@
1-
import { createOpenAI } from "@ai-sdk/openai";
2-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
31
import { streamText, UIMessage, convertToModelMessages } from "ai";
2+
import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models";
3+
import { buildSystemMessage } from "@/lib/ai/prompt";
44

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

8-
export async function POST(req: Request) {
9-
const {
10-
messages,
11-
system,
12-
pageContext,
13-
provider,
14-
apiKey,
15-
}: {
16-
messages: UIMessage[];
17-
system?: string; // System message forwarded from AssistantChatTransport
18-
tools?: unknown; // Frontend tools forwarded from AssistantChatTransport
19-
pageContext?: {
20-
title?: string;
21-
description?: string;
22-
content?: string;
23-
slug?: string;
24-
};
25-
provider?: "openai" | "gemini";
26-
apiKey?: string;
27-
} = await req.json();
28-
29-
// Check if API key is provided
30-
if (!apiKey || apiKey.trim() === "") {
31-
return Response.json(
32-
{
33-
error:
34-
"API key is required. Please configure your API key in the settings.",
35-
},
36-
{ status: 400 },
37-
);
38-
}
8+
interface ChatRequest {
9+
messages: UIMessage[];
10+
system?: string;
11+
tools?: unknown;
12+
pageContext?: {
13+
title?: string;
14+
description?: string;
15+
content?: string;
16+
slug?: string;
17+
};
18+
provider?: AIProvider;
19+
apiKey?: string;
20+
}
3921

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

48-
// Add current page context if available
49-
if (pageContext?.content) {
50-
systemMessage += `\n\n--- CURRENT PAGE CONTEXT ---\n`;
51-
if (pageContext.title) {
52-
systemMessage += `Page Title: ${pageContext.title}\n`;
53-
}
54-
if (pageContext.description) {
55-
systemMessage += `Page Description: ${pageContext.description}\n`;
56-
}
57-
if (pageContext.slug) {
58-
systemMessage += `Page URL: /docs/${pageContext.slug}\n`;
59-
}
60-
systemMessage += `Page Content:\n${pageContext.content}`;
61-
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.`;
32+
// 对指定Provider验证key是否存在
33+
if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) {
34+
return Response.json(
35+
{
36+
error:
37+
"API key is required. Please configure your API key in the settings.",
38+
},
39+
{ status: 400 },
40+
);
6241
}
6342

64-
// Select model based on provider
65-
let model;
66-
if (provider === "gemini") {
67-
const customGoogle = createGoogleGenerativeAI({
68-
apiKey: apiKey,
69-
});
70-
model = customGoogle("models/gemini-2.0-flash");
71-
} else {
72-
// Default to OpenAI
73-
const customOpenAI = createOpenAI({
74-
apiKey: apiKey,
75-
});
76-
model = customOpenAI("gpt-4.1-nano");
77-
}
43+
// 构建系统消息,包含页面上下文
44+
const systemMessage = buildSystemMessage(system, pageContext);
45+
46+
// 根据Provider获取 AI 模型实例
47+
const model = getModel(provider, apiKey);
7848

49+
// 生成流式响应
7950
const result = streamText({
8051
model: model,
8152
system: systemMessage,
@@ -85,6 +56,12 @@ export async function POST(req: Request) {
8556
return result.toUIMessageStreamResponse();
8657
} catch (error) {
8758
console.error("Chat API error:", error);
59+
60+
// 处理特定模型创建错误
61+
if (error instanceof Error && error.message.includes("API key")) {
62+
return Response.json({ error: error.message }, { status: 400 });
63+
}
64+
8865
return Response.json(
8966
{ error: "Failed to process chat request" },
9067
{ status: 500 },

app/components/DocsAssistant.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
5353
const currentApiKey =
5454
currentProvider === "openai"
5555
? openaiApiKeyRef.current
56-
: geminiApiKeyRef.current;
56+
: currentProvider === "gemini"
57+
? geminiApiKeyRef.current
58+
: ""; // intern provider doesn't need API key
5759

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

119121
function deriveAssistantError(
120122
err: unknown,
121-
provider: "openai" | "gemini",
123+
provider: "openai" | "gemini" | "intern",
122124
): AssistantErrorState {
123-
const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI";
125+
const providerLabel =
126+
provider === "gemini"
127+
? "Google Gemini"
128+
: provider === "intern"
129+
? "Intern-AI"
130+
: "OpenAI";
124131
const fallback: AssistantErrorState = {
125132
message:
126133
"The assistant couldn't complete that request. Please try again later.",
@@ -176,14 +183,16 @@ function deriveAssistantError(
176183

177184
let showSettingsCTA = false;
178185

186+
// For intern provider, don't show settings CTA for API key related errors
179187
if (
180-
statusCode === 400 ||
181-
statusCode === 401 ||
182-
statusCode === 403 ||
183-
normalized.includes("api key") ||
184-
normalized.includes("apikey") ||
185-
normalized.includes("missing key") ||
186-
normalized.includes("unauthorized")
188+
provider !== "intern" &&
189+
(statusCode === 400 ||
190+
statusCode === 401 ||
191+
statusCode === 403 ||
192+
normalized.includes("api key") ||
193+
normalized.includes("apikey") ||
194+
normalized.includes("missing key") ||
195+
normalized.includes("unauthorized"))
187196
) {
188197
showSettingsCTA = true;
189198
}

app/components/assistant-ui/SettingsDialog.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ export const SettingsDialog = ({
5151
<RadioGroup
5252
value={provider}
5353
onValueChange={(value) =>
54-
setProvider(value as "openai" | "gemini")
54+
setProvider(value as "openai" | "gemini" | "intern")
5555
}
5656
>
57+
<div className="flex items-center space-x-2">
58+
<RadioGroupItem value="intern" id="intern" />
59+
<Label htmlFor="intern">InternS1 (Free)</Label>
60+
</div>
5761
<div className="flex items-center space-x-2">
5862
<RadioGroupItem value="openai" id="openai" />
5963
<Label htmlFor="openai">OpenAI</Label>
@@ -90,6 +94,15 @@ export const SettingsDialog = ({
9094
/>
9195
</div>
9296
)}
97+
98+
{provider === "intern" && (
99+
<div className="space-y-2">
100+
<div className="text-sm text-muted-foreground">
101+
感谢上海AILab的书生大模型对本项目的算力支持,Intern-AI
102+
模型已预配置,无需提供 API Key。
103+
</div>
104+
</div>
105+
)}
93106
</div>
94107

95108
<DialogFooter>

app/components/assistant-ui/assistant-modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const AssistantModal: FC<AssistantModalProps> = ({
2121
}) => {
2222
return (
2323
<AssistantModalPrimitive.Root>
24-
<AssistantModalPrimitive.Anchor className="aui-root aui-modal-anchor fixed right-4 bottom-4 size-11">
24+
<AssistantModalPrimitive.Anchor className="aui-root aui-modal-anchor fixed right-4 bottom-4 size-14">
2525
<AssistantModalPrimitive.Trigger asChild>
2626
<AssistantModalButton />
2727
</AssistantModalPrimitive.Trigger>
@@ -59,12 +59,12 @@ const AssistantModalButton = forwardRef<
5959
>
6060
<BotIcon
6161
data-state={state}
62-
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"
62+
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"
6363
/>
6464

6565
<ChevronDownIcon
6666
data-state={state}
67-
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"
67+
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"
6868
/>
6969
<span className="aui-sr-only sr-only">{tooltip}</span>
7070
</TooltipIconButton>

app/components/assistant-ui/thread.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,17 +268,27 @@ const Composer: FC<ComposerProps> = ({
268268
onClearError,
269269
}) => {
270270
const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings();
271-
const activeKey = provider === "openai" ? openaiApiKey : geminiApiKey;
272-
const hasActiveKey = activeKey.trim().length > 0;
273-
const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI";
271+
const activeKey =
272+
provider === "openai"
273+
? openaiApiKey
274+
: provider === "gemini"
275+
? geminiApiKey
276+
: "";
277+
const hasActiveKey = provider === "intern" || activeKey.trim().length > 0;
278+
const providerLabel =
279+
provider === "gemini"
280+
? "Google Gemini"
281+
: provider === "intern"
282+
? "Intern-AI"
283+
: "OpenAI";
274284

275285
const handleOpenSettings = useCallback(() => {
276286
onClearError?.();
277287
onOpenChange(true);
278288
}, [onClearError, onOpenChange]);
279289

280290
return (
281-
<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">
291+
<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">
282292
<ThreadScrollToBottom />
283293
<ThreadPrimitive.Empty>
284294
<ThreadWelcomeSuggestions />

app/hooks/useAssistantSettings.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from "react";
1111
import type { ReactNode } from "react";
1212

13-
type Provider = "openai" | "gemini";
13+
type Provider = "openai" | "gemini" | "intern";
1414

1515
interface AssistantSettingsState {
1616
provider: Provider;
@@ -45,7 +45,12 @@ const parseStoredSettings = (raw: string | null): AssistantSettingsState => {
4545
try {
4646
const parsed = JSON.parse(raw) as Partial<AssistantSettingsState>;
4747
return {
48-
provider: parsed.provider === "gemini" ? "gemini" : "openai",
48+
provider:
49+
parsed.provider === "gemini"
50+
? "gemini"
51+
: parsed.provider === "intern"
52+
? "intern"
53+
: "openai",
4954
openaiApiKey:
5055
typeof parsed.openaiApiKey === "string" ? parsed.openaiApiKey : "",
5156
geminiApiKey:

lib/ai/models.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createOpenAIModel } from "./providers/openai";
2+
import { createGeminiModel } from "./providers/gemini";
3+
import { createInternModel } from "./providers/intern";
4+
5+
export type AIProvider = "openai" | "gemini" | "intern";
6+
7+
/**
8+
* Model工厂 用于返回对应的 AI 模型实例
9+
* @param provider - 要用的provider
10+
* @param apiKey - API key (intern provider不需要用户提供 API key)
11+
* @returns 配置好的 AI 模型实例
12+
*/
13+
export function getModel(provider: AIProvider, apiKey?: string) {
14+
switch (provider) {
15+
case "openai":
16+
if (!apiKey || apiKey.trim() === "") {
17+
throw new Error("OpenAI API key is required");
18+
}
19+
return createOpenAIModel(apiKey);
20+
21+
case "gemini":
22+
if (!apiKey || apiKey.trim() === "") {
23+
throw new Error("Gemini API key is required");
24+
}
25+
return createGeminiModel(apiKey);
26+
27+
case "intern":
28+
// Intern 书生模型不需要用户提供 API key
29+
return createInternModel();
30+
31+
default:
32+
throw new Error(`Unsupported AI provider: ${provider}`);
33+
}
34+
}
35+
36+
/**
37+
* 检查指定的提供者是否需要用户提供 API key
38+
* @param provider - 要检查的provider
39+
* @returns 如果需要 API key,返回 true,否则返回 false
40+
*/
41+
export function requiresApiKey(provider: AIProvider): boolean {
42+
return provider !== "intern";
43+
}

lib/ai/prompt.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
interface PageContext {
2+
title?: string;
3+
description?: string;
4+
content?: string;
5+
slug?: string;
6+
}
7+
8+
/**
9+
* 构建系统消息,包含页面上下文
10+
* @param customSystem - 自定义系统消息 (可选)
11+
* @param pageContext - 当前页面上下文 (可选)
12+
* @returns 完整的系统消息字符串
13+
*/
14+
export function buildSystemMessage(
15+
customSystem?: string,
16+
pageContext?: PageContext,
17+
): string {
18+
// 默认系统消息
19+
let systemMessage =
20+
customSystem ||
21+
`You are a helpful AI assistant for a documentation website.
22+
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.
23+
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.`;
24+
25+
// 如果当前页面上下文可用,则添加到系统消息中
26+
if (pageContext?.content) {
27+
systemMessage += `\n\n--- CURRENT PAGE CONTEXT ---\n`;
28+
29+
if (pageContext.title) {
30+
systemMessage += `Page Title: ${pageContext.title}\n`;
31+
}
32+
33+
if (pageContext.description) {
34+
systemMessage += `Page Description: ${pageContext.description}\n`;
35+
}
36+
37+
if (pageContext.slug) {
38+
systemMessage += `Page URL: /docs/${pageContext.slug}\n`;
39+
}
40+
41+
systemMessage += `Page Content:\n${pageContext.content}`;
42+
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.`;
43+
}
44+
45+
return systemMessage;
46+
}

0 commit comments

Comments
 (0)