Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
846da09
chore: update dependencies in package.json and pnpm-lock.yaml
Crokily Sep 18, 2025
ca7b154
chore: update component paths in components.json and add new dependen…
Crokily Sep 18, 2025
b578f62
chore: add zod dependency and update pnpm-lock.yaml
Crokily Sep 18, 2025
98c1ccc
refactor: remove unnecessary background class from Composer component
Crokily Sep 18, 2025
a4f6d44
feat: enhance chat API with page context and update DocsAssistant to …
Crokily Sep 18, 2025
cbb258b
refactor: remove console logs from chat API and DocsAssistant components
Crokily Sep 18, 2025
11dc180
feat: integrate Google AI SDK and enhance chat API with provider support
Crokily Sep 18, 2025
77d5085
fix: update import path for RadioGroup in SettingsDialog component
Crokily Sep 18, 2025
23d3c86
refactor: streamline chat API imports and enhance DocsAssistant with …
Crokily Sep 18, 2025
a695090
refactor: remove unused ComposerAddAttachment import from thread comp…
Crokily Sep 18, 2025
ababd4d
refactor: enhance Composer component with API key validation and sett…
Crokily Sep 18, 2025
966141b
feat: enhance DocsAssistant and AssistantModal with error handling an…
Crokily Sep 18, 2025
96e998d
feat: add refresh functionality to settings and improve DocsAssistant…
Crokily Sep 18, 2025
e1702dc
feat: add default tag to new article creation in Contribute component
Crokily Sep 18, 2025
9a44b99
feat: update welcome suggestions in Thread component with Chinese pro…
Crokily Sep 18, 2025
7b270d5
Merge branch 'main' into feat/AIplus
Crokily Sep 18, 2025
43803c1
fix: update front matter formatting in Contribute component and wrap …
Crokily Sep 18, 2025
1d52fa2
chore: update README (trigger re-deploy to Vercel testing environment)
Crokily Sep 19, 2025
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pnpm dev
├── 📂 app/ # Next.js App Router
│ ├── 📂 components/ # React 组件
│ ├── 📂 docs/ # 文档内容
│ │ └── 📂 computer-science/ # 计算机科学知识库
│ │ └── 📂 ai/ # ai知识库
│ ├── 📄 layout.tsx # 根布局
│ └── 📄 page.tsx # 主页
├── 📂 source.config.ts # Fumadocs 配置
Expand Down
93 changes: 93 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createOpenAI } from "@ai-sdk/openai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { streamText, UIMessage, convertToModelMessages } from "ai";

// Allow streaming responses up to 30 seconds
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 },
);
}

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.`;

// 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.`;
}

// 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 result = streamText({
model: model,
system: systemMessage,
messages: convertToModelMessages(messages),
});

return result.toUIMessageStreamResponse();
} catch (error) {
console.error("Chat API error:", error);
return Response.json(
{ error: "Failed to process chat request" },
{ status: 500 },
);
}
}
9 changes: 5 additions & 4 deletions app/components/Contribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ type DirNode = { name: string; path: string; children?: DirNode[] };
function buildGithubNewUrl(dirPath: string, filename: string, title: string) {
const file = filename.endsWith(".mdx") ? filename : `${filename}.mdx`;
const frontMatter = `---
title: ${title || "New Article"}
description:
date: ${new Date().toISOString().slice(0, 10)}
tags: []
title: '${title || "New Article"}'
description: ""
date: "${new Date().toISOString().slice(0, 10)}"
tags:
- tag-one
---

# ${title || "New Article"}
Expand Down
235 changes: 235 additions & 0 deletions app/components/DocsAssistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
"use client";

import { useCallback, useEffect, useState, useRef } from "react";

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal";
import {
AssistantSettingsProvider,
useAssistantSettings,
} from "@/app/hooks/useAssistantSettings";

interface PageContext {
title?: string;
description?: string;
content?: string;
slug?: string;
}

interface DocsAssistantProps {
pageContext: PageContext;
}

export function DocsAssistant({ pageContext }: DocsAssistantProps) {
return (
<AssistantSettingsProvider>
<DocsAssistantInner pageContext={pageContext} />
</AssistantSettingsProvider>
);
}

function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings();

// Use refs to ensure we always get the latest values
const providerRef = useRef(provider);
const openaiApiKeyRef = useRef(openaiApiKey);
const geminiApiKeyRef = useRef(geminiApiKey);

// Update refs whenever the values change
providerRef.current = provider;
openaiApiKeyRef.current = openaiApiKey;
geminiApiKeyRef.current = geminiApiKey;

const chat = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
body: () => {
// Use refs to get the current values at request time
const currentProvider = providerRef.current;
const currentApiKey =
currentProvider === "openai"
? openaiApiKeyRef.current
: geminiApiKeyRef.current;

console.log("[DocsAssistant] useChat body function called with:", {
provider: currentProvider,
apiKeyLength: currentApiKey.length,
hasApiKey: currentApiKey.trim().length > 0,
});

return {
pageContext,
provider: currentProvider,
apiKey: currentApiKey,
};
},
}),
});

const {
error: chatError,
status: chatStatus,
clearError: clearChatError,
} = chat;
const [assistantError, setAssistantError] =
useState<AssistantErrorState | null>(null);

useEffect(() => {
if (!chatError) {
return;
}

setAssistantError(deriveAssistantError(chatError, provider));
clearChatError();
}, [chatError, clearChatError, provider]);

useEffect(() => {
if (chatStatus === "submitted" || chatStatus === "streaming") {
setAssistantError(null);
}
}, [chatStatus]);

const handleClearError = useCallback(() => {
setAssistantError(null);
clearChatError();
}, [clearChatError]);

const runtime = useAISDKRuntime(chat);

return (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantModal
errorMessage={assistantError?.message}
showSettingsAction={assistantError?.showSettingsCTA ?? false}
onClearError={assistantError ? handleClearError : undefined}
/>
</AssistantRuntimeProvider>
);
}

interface AssistantErrorState {
message: string;
showSettingsCTA: boolean;
}

function deriveAssistantError(
err: unknown,
provider: "openai" | "gemini",
): AssistantErrorState {
const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI";
const fallback: AssistantErrorState = {
message:
"The assistant couldn't complete that request. Please try again later.",
showSettingsCTA: false,
};

if (!err) {
return fallback;
}

const maybeError = err as Partial<{
message?: string;
statusCode?: number;
responseBody?: string;
data?: unknown;
}>;

let message = "";

if (
typeof maybeError.message === "string" &&
maybeError.message.trim().length > 0
) {
message = maybeError.message.trim();
}

if (
typeof maybeError.responseBody === "string" &&
maybeError.responseBody.trim().length > 0
) {
const extracted = extractErrorFromResponseBody(maybeError.responseBody);
if (extracted) {
message = extracted;
}
}

if (!message && err instanceof Error && typeof err.message === "string") {
message = err.message.trim();
}

if (!message && maybeError.data && typeof maybeError.data === "object") {
const dataError = (maybeError.data as { error?: unknown }).error;
if (typeof dataError === "string" && dataError.trim().length > 0) {
message = dataError.trim();
}
}

const statusCode =
typeof maybeError.statusCode === "number"
? maybeError.statusCode
: undefined;
const normalized = message.toLowerCase();

let showSettingsCTA = false;

if (
statusCode === 400 ||
statusCode === 401 ||
statusCode === 403 ||
normalized.includes("api key") ||
normalized.includes("apikey") ||
normalized.includes("missing key") ||
normalized.includes("unauthorized")
) {
showSettingsCTA = true;
}

let friendlyMessage = message || fallback.message;

if (showSettingsCTA) {
friendlyMessage =
message && message.length > 0
? message
: `The ${providerLabel} API key looks incorrect. Update it in settings and try again.`;
} else if (statusCode === 429) {
friendlyMessage =
"The provider is rate limiting requests. Please wait and try again.";
} else if (statusCode && statusCode >= 500) {
friendlyMessage =
"The AI provider is currently unavailable. Please try again soon.";
}

return {
message: friendlyMessage,
showSettingsCTA,
};
}

function extractErrorFromResponseBody(body: string): string | undefined {
const trimmed = body.trim();
if (!trimmed) {
return undefined;
}

try {
const parsed = JSON.parse(trimmed);
if (typeof parsed === "string") {
return parsed.trim();
}
if (
parsed &&
typeof parsed === "object" &&
typeof (parsed as { error?: unknown }).error === "string"
) {
return (parsed as { error: string }).error.trim();
}
} catch {
// Ignore JSON parsing issues and fall back to the raw body text.
}

return trimmed;
}
14 changes: 14 additions & 0 deletions app/components/assistant-ui/SettingsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SettingsIcon } from "lucide-react";
import { TooltipIconButton } from "./tooltip-icon-button";

interface SettingsButtonProps {
onClick: () => void;
}

export const SettingsButton = ({ onClick }: SettingsButtonProps) => {
return (
<TooltipIconButton tooltip="Settings" side="top" onClick={onClick}>
<SettingsIcon className="size-5" />
</TooltipIconButton>
);
};
Loading