mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-23 06:00:17 +09:00
feat: web search
This commit is contained in:
parent
657e44b501
commit
59dd3213f8
17
README.md
17
README.md
@ -354,19 +354,28 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
|
||||
|
||||
### `DEEPSEEK_API_KEY` (可选)
|
||||
|
||||
DeepSeek Api Key.
|
||||
DeepSeek Api Key
|
||||
|
||||
### `DEEPSEEK_URL` (可选)
|
||||
|
||||
DeepSeek Api Url.
|
||||
DeepSeek Api Url
|
||||
|
||||
### `SILICONFLOW_API_KEY` (可选)
|
||||
|
||||
硅基流动 API Key.
|
||||
硅基流动 API Key
|
||||
|
||||
### `SILICONFLOW_URL` (可选)
|
||||
|
||||
硅基流动 API URL.
|
||||
硅基流动 API URL
|
||||
|
||||
### `TAVILY_API_KEY`
|
||||
|
||||
Tavily API Key 用于通用搜索功能
|
||||
获取地址:https://tavily.com
|
||||
|
||||
### `TAVILY_MAX_RETURNS` (可选)
|
||||
|
||||
通用搜索功能返回的最大结果数,默认为 10
|
||||
|
||||
## 部署
|
||||
|
||||
|
@ -19,6 +19,7 @@ const DANGER_CONFIG = {
|
||||
isUseOpenAIEndpointForAllModels: serverConfig.isUseOpenAIEndpointForAllModels,
|
||||
disableModelProviderDisplay: serverConfig.disableModelProviderDisplay,
|
||||
isUseRemoteModels: serverConfig.isUseRemoteModels,
|
||||
isEnableWebSearch: serverConfig.isEnableWebSearch,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
71
app/api/search/route.ts
Normal file
71
app/api/search/route.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../auth";
|
||||
import { ModelProvider } from "@/app/constant";
|
||||
import { tavily } from "@tavily/core";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
async function handle(req: NextRequest) {
|
||||
const authResult = auth(req, ModelProvider.GPT);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const tavilyApiKey = serverConfig.tavilyApiKey;
|
||||
const maxReturns = serverConfig.tavilyMaxReturns
|
||||
? parseInt(serverConfig.tavilyMaxReturns, 10)
|
||||
: 10;
|
||||
|
||||
if (!tavilyApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: "Tavily API key not configured",
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { query } = body;
|
||||
|
||||
if (!query) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: "Search query is required",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
const tvly = tavily({ apiKey: tavilyApiKey });
|
||||
const response = await tvly.search(query, {
|
||||
maxResults: maxReturns,
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("[Tavily] search error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: "Failed to process search request",
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
@ -25,6 +25,7 @@ import { DeepSeekApi } from "./platforms/deepseek";
|
||||
import { XAIApi } from "./platforms/xai";
|
||||
import { ChatGLMApi } from "./platforms/glm";
|
||||
import { SiliconflowApi } from "./platforms/siliconflow";
|
||||
import { TavilySearchResponse } from "@tavily/core";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
@ -45,6 +46,7 @@ export interface RequestMessage {
|
||||
role: MessageRole;
|
||||
content: string | MultimodalContent[];
|
||||
fileInfos?: FileInfo[];
|
||||
webSearchReferences?: TavilySearchResponse;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
|
@ -25,7 +25,10 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
@ -104,7 +107,7 @@ export class QwenApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: getMessageTextContent(v),
|
||||
content: getWebReferenceMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
|
@ -22,7 +22,11 @@ import {
|
||||
} from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { ANTHROPIC_BASE_URL } from "@/app/constant";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { RequestPayload } from "./openai";
|
||||
@ -318,7 +322,7 @@ export class ClaudeApi implements LLMApi {
|
||||
if (!visionModel || typeof content === "string") {
|
||||
return {
|
||||
role: insideRole,
|
||||
content: getMessageTextContent(v),
|
||||
content: getWebReferenceMessageTextContent(v),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
@ -26,7 +26,10 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
@ -97,7 +100,7 @@ export class ErnieApi implements LLMApi {
|
||||
const messages = options.messages.map((v) => ({
|
||||
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
|
||||
role: v.role === "system" ? "user" : v.role,
|
||||
content: getMessageTextContent(v),
|
||||
content: getWebReferenceMessageTextContent(v),
|
||||
}));
|
||||
|
||||
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",
|
||||
|
@ -25,7 +25,10 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
@ -98,7 +101,7 @@ export class DoubaoApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
content: getMessageTextContent(v),
|
||||
content: getWebReferenceMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
|
@ -28,6 +28,7 @@ import { getClientConfig } from "@/app/config/client";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageTextContentWithoutThinking,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
@ -86,7 +87,7 @@ export class DeepSeekApi implements LLMApi {
|
||||
const content = getMessageTextContentWithoutThinking(v);
|
||||
messages.push({ role: v.role, content });
|
||||
} else {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ import {
|
||||
TranscriptionOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
@ -78,7 +81,7 @@ export class ChatGLMApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { preProcessImageContent } from "@/app/utils/chat";
|
||||
import { nanoid } from "nanoid";
|
||||
@ -91,7 +92,7 @@ export class GeminiProApi implements LLMApi {
|
||||
_messages.push({ role: v.role, content });
|
||||
}
|
||||
const messages = _messages.map((v) => {
|
||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||
let parts: any[] = [{ text: getWebReferenceMessageTextContent(v) }];
|
||||
if (isVisionModel(options.config.model)) {
|
||||
const images = getMessageImages(v);
|
||||
if (images.length > 0) {
|
||||
|
@ -24,7 +24,10 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
import { RequestPayload } from "./openai";
|
||||
@ -79,7 +82,7 @@ export class SparkApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,10 @@ import {
|
||||
TranscriptionOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
@ -79,7 +82,7 @@ export class MoonshotApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
getMessageTextContent,
|
||||
isVisionModel,
|
||||
isDalle3 as _isDalle3,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
@ -239,7 +240,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
for (const v of options.messages) {
|
||||
const content = visionModel
|
||||
? await preProcessImageContent(v.content)
|
||||
: getMessageTextContent(v);
|
||||
: getWebReferenceMessageTextContent(v);
|
||||
if (!(isO1 && v.role === "system"))
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import { getClientConfig } from "@/app/config/client";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageTextContentWithoutThinking,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
@ -89,7 +90,7 @@ export class SiliconflowApi implements LLMApi {
|
||||
const content = getMessageTextContentWithoutThinking(v);
|
||||
messages.push({ role: v.role, content });
|
||||
} else {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,11 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
import mapKeys from "lodash-es/mapKeys";
|
||||
import mapValues from "lodash-es/mapValues";
|
||||
import isArray from "lodash-es/isArray";
|
||||
@ -110,7 +114,7 @@ export class HunyuanApi implements LLMApi {
|
||||
const messages = options.messages.map((v, index) => ({
|
||||
// "Messages 中 system 角色必须位于列表的最开始"
|
||||
role: index !== 0 && v.role === "system" ? "user" : v.role,
|
||||
content: visionModel ? v.content : getMessageTextContent(v),
|
||||
content: visionModel ? v.content : getWebReferenceMessageTextContent(v),
|
||||
}));
|
||||
|
||||
const modelConfig = {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TavilySearchResponse } from "@tavily/core";
|
||||
import { ClientApi, getClientApi, getHeaders } from "../api";
|
||||
import { ChatSession } from "@/app/store";
|
||||
|
||||
@ -45,3 +46,19 @@ export class FileApi {
|
||||
return fileInfo;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebApi {
|
||||
async search(query: string): Promise<TavilySearchResponse> {
|
||||
var headers = getHeaders(true);
|
||||
const api = "/api/search";
|
||||
var res = await fetch(api, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query }),
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
const resJson = await res.json();
|
||||
return resJson;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,10 @@ import {
|
||||
TranscriptionOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getWebReferenceMessageTextContent,
|
||||
} from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
@ -74,7 +77,7 @@ export class XAIApi implements LLMApi {
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
const content = getWebReferenceMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,8 @@ import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
import HeadphoneIcon from "../icons/headphone.svg";
|
||||
import SearchCloseIcon from "../icons/search_close.svg";
|
||||
import SearchOpenIcon from "../icons/search_open.svg";
|
||||
import {
|
||||
ChatMessage,
|
||||
SubmitKey,
|
||||
@ -509,6 +511,17 @@ export function ChatActions(props: {
|
||||
const pluginStore = usePluginStore();
|
||||
const session = chatStore.currentSession();
|
||||
|
||||
// switch web search
|
||||
const webSearch = chatStore.currentSession().mask.webSearch;
|
||||
function switchWebSearch() {
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.mask.webSearch =
|
||||
!session.mask.webSearch &&
|
||||
!isFunctionCallModel(currentModel) &&
|
||||
isEnableWebSearch;
|
||||
});
|
||||
}
|
||||
|
||||
// switch Plugins
|
||||
const usePlugins = chatStore.currentSession().mask.usePlugins;
|
||||
function switchUsePlugins() {
|
||||
@ -593,6 +606,11 @@ export function ChatActions(props: {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
const isEnableWebSearch = useMemo(
|
||||
() => accessStore.enableWebSearch(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const show = isVisionModel(currentModel);
|
||||
@ -723,6 +741,17 @@ export function ChatActions(props: {
|
||||
text={currentModelName}
|
||||
icon={<RobotIcon />}
|
||||
/>
|
||||
{!isFunctionCallModel(currentModel) && isEnableWebSearch && (
|
||||
<ChatAction
|
||||
onClick={switchWebSearch}
|
||||
text={
|
||||
webSearch
|
||||
? Locale.Chat.InputActions.CloseWebSearch
|
||||
: Locale.Chat.InputActions.OpenWebSearch
|
||||
}
|
||||
icon={webSearch ? <SearchOpenIcon /> : <SearchCloseIcon />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showModelSelector && (
|
||||
<SearchSelector
|
||||
@ -1351,7 +1380,12 @@ function _Chat() {
|
||||
const textContent = getMessageTextContent(userMessage);
|
||||
const images = getMessageImages(userMessage);
|
||||
chatStore
|
||||
.onUserInput(textContent, images, userMessage.fileInfos)
|
||||
.onUserInput(
|
||||
textContent,
|
||||
images,
|
||||
userMessage.fileInfos,
|
||||
userMessage.webSearchReferences,
|
||||
)
|
||||
.then(() => setIsLoading(false));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
@ -1432,34 +1466,36 @@ function _Chat() {
|
||||
|
||||
// preview messages
|
||||
const renderMessages = useMemo(() => {
|
||||
return context
|
||||
.concat(session.messages as RenderMessage[])
|
||||
.concat(
|
||||
isLoading
|
||||
? [
|
||||
{
|
||||
...createMessage({
|
||||
role: "assistant",
|
||||
content: "……",
|
||||
}),
|
||||
preview: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
.concat(
|
||||
userInput.length > 0 && config.sendPreviewBubble
|
||||
? [
|
||||
{
|
||||
...createMessage({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
}),
|
||||
preview: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
return (
|
||||
context
|
||||
.concat(session.messages as RenderMessage[])
|
||||
// .concat(
|
||||
// isLoading
|
||||
// ? [
|
||||
// {
|
||||
// ...createMessage({
|
||||
// role: "assistant",
|
||||
// content: "……",
|
||||
// }),
|
||||
// preview: true,
|
||||
// },
|
||||
// ]
|
||||
// : [],
|
||||
// )
|
||||
.concat(
|
||||
userInput.length > 0 && config.sendPreviewBubble
|
||||
? [
|
||||
{
|
||||
...createMessage({
|
||||
role: "user",
|
||||
content: userInput,
|
||||
}),
|
||||
preview: true,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
);
|
||||
}, [
|
||||
config.sendPreviewBubble,
|
||||
context,
|
||||
@ -2093,6 +2129,7 @@ function _Chat() {
|
||||
<Markdown
|
||||
key={message.streaming ? "loading" : "done"}
|
||||
content={getMessageTextContent(message)}
|
||||
webSearchReferences={message.webSearchReferences}
|
||||
loading={
|
||||
(message.preview || message.streaming) &&
|
||||
message.content.length === 0 &&
|
||||
@ -2140,9 +2177,9 @@ function _Chat() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{message?.audio_url && (
|
||||
{message?.audioUrl && (
|
||||
<div className={styles["chat-message-audio"]}>
|
||||
<audio src={message.audio_url} controls />
|
||||
<audio src={message.audioUrl} controls />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import { useChatStore } from "../store";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { TavilySearchResponse } from "@tavily/core";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -273,10 +274,20 @@ function tryWrapHtmlCode(text: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
function _MarkDownContent(props: {
|
||||
content: string;
|
||||
webSearchReferences?: TavilySearchResponse;
|
||||
}) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||
}, [props.content]);
|
||||
let content = tryWrapHtmlCode(escapeBrackets(props.content));
|
||||
if (props.webSearchReferences?.results) {
|
||||
content = content.replace(/\[citation:(\d+)\]/g, (match, index) => {
|
||||
const result = props.webSearchReferences?.results[parseInt(index) - 1];
|
||||
return result ? `[\[${index}\]](${result.url})` : match;
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}, [props.content, props.webSearchReferences]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
@ -332,6 +343,7 @@ export function Markdown(
|
||||
fontFamily?: string;
|
||||
parentRef?: RefObject<HTMLDivElement>;
|
||||
defaultShow?: boolean;
|
||||
webSearchReferences?: TavilySearchResponse;
|
||||
} & React.DOMAttributes<HTMLDivElement>,
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
@ -351,7 +363,10 @@ export function Markdown(
|
||||
{props.loading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<MarkdownContent content={props.content} />
|
||||
<MarkdownContent
|
||||
content={props.content}
|
||||
webSearchReferences={props.webSearchReferences}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -191,11 +191,11 @@ export function RealtimeChat({
|
||||
});
|
||||
}
|
||||
if (hasAudio) {
|
||||
// upload audio get audio_url
|
||||
// upload audio get audioUrl
|
||||
const blob = audioHandlerRef.current?.savePlayFile();
|
||||
uploadImage(blob!).then((audio_url) => {
|
||||
botMessage.audio_url = audio_url;
|
||||
// update text and audio_url
|
||||
uploadImage(blob!).then((audioUrl) => {
|
||||
botMessage.audioUrl = audioUrl;
|
||||
// update text and audioUrl
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
@ -215,15 +215,15 @@ export function RealtimeChat({
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat([userMessage]);
|
||||
});
|
||||
// save input audio_url, and update session
|
||||
// save input audioUrl, and update session
|
||||
const { audioStartMillis, audioEndMillis } = item;
|
||||
// upload audio get audio_url
|
||||
// upload audio get audioUrl
|
||||
const blob = audioHandlerRef.current?.saveRecordFile(
|
||||
audioStartMillis,
|
||||
audioEndMillis,
|
||||
);
|
||||
uploadImage(blob!).then((audio_url) => {
|
||||
userMessage.audio_url = audio_url;
|
||||
uploadImage(blob!).then((audioUrl) => {
|
||||
userMessage.audioUrl = audioUrl;
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
|
@ -285,5 +285,9 @@ export const getServerSideConfig = () => {
|
||||
|
||||
disableModelProviderDisplay: !!process.env.DISABLE_MODEL_PROVIDER_DISPLAY,
|
||||
isUseRemoteModels: !!process.env.USE_REMOTE_MODELS,
|
||||
|
||||
tavilyApiKey: process.env.TAVILY_API_KEY,
|
||||
tavilyMaxReturns: process.env.TAVILY_MAX_RETURNS ?? "10",
|
||||
isEnableWebSearch: !!process.env.TAVILY_API_KEY,
|
||||
};
|
||||
};
|
||||
|
@ -662,30 +662,3 @@ export const internalAllowedWebDavEndpoints = [
|
||||
];
|
||||
|
||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
||||
|
||||
export const MYFILES_BROWSER_TOOLS_SYSTEM_PROMPT = `
|
||||
# Tools
|
||||
|
||||
## myfiles_browser
|
||||
|
||||
You have the tool 'myfiles_browser' with the following functions:
|
||||
issues queries to search the file(s) uploaded in the current conversation and displays the results.
|
||||
|
||||
This tool is for browsing the files uploaded by the user.
|
||||
|
||||
Parts of the documents uploaded by users will be automatically included in the conversation. Only use this tool when the relevant parts don't contain the necessary information to fulfill the user's request.
|
||||
|
||||
If the user needs to summarize the document, they can summarize it through parts of the document.
|
||||
|
||||
Think carefully about how the information you find relates to the user's request. Respond as soon as you find information that clearly answers the request.
|
||||
|
||||
Issue multiple queries to the 'myfiles_browser' command only when the user's question needs to be decomposed to find different facts. In other scenarios, prefer providing a single query. Avoid single-word queries that are extremely broad and will return unrelated results.
|
||||
|
||||
Here are some examples of how to use the 'myfiles_browser' command:
|
||||
User: What was the GDP of France and Italy in the 1970s? => myfiles_browser(["france gdp 1970", "italy gdp 1970"])
|
||||
User: What does the report say about the GPT4 performance on MMLU? => myfiles_browser(["GPT4 MMLU performance"])
|
||||
User: How can I integrate customer relationship management system with third-party email marketing tools? => myfiles_browser(["customer management system marketing integration"])
|
||||
User: What are the best practices for data security and privacy for our cloud storage services? => myfiles_browser(["cloud storage security and privacy"])
|
||||
|
||||
The user has uploaded the following files:
|
||||
`;
|
||||
|
1
app/icons/search_close.svg
Normal file
1
app/icons/search_close.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683102619308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7254" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M533.333333 42.666667C262.346667 42.666667 42.666667 262.346667 42.666667 533.333333s219.68 490.666667 490.666666 490.666667 490.666667-219.68 490.666667-490.666667S804.32 42.666667 533.333333 42.666667z m342.086667 760.253333c-29.266667-16-61.566667-29.633333-96.166667-40.853333 18.713333-63.493333 29.413333-134 31.153334-207.4h170.42a445.853333 445.853333 0 0 1-94.413334 254.446666q-5.4-3.113333-10.993333-6.193333z m-695.166667 6.193333A445.853333 445.853333 0 0 1 85.84 554.666667h170.42c1.74 73.373333 12.44 143.906667 31.153333 207.4-34.6 11.22-66.9 24.886667-96.166666 40.853333q-5.593333 3.08-10.993334 6.193333z m10.993334-545.366666c29.266667 15.966667 61.566667 29.633333 96.166666 40.853333-18.713333 63.493333-29.413333 134-31.153333 207.4H85.84a445.866667 445.866667 0 0 1 94.413333-254.446667q5.4 3.113333 10.993334 6.193334zM554.666667 341.073333c64.34-1.526667 126.5-9.94 183.64-24.606666 17.6 59.6 27.706667 126.106667 29.426666 195.533333H554.666667z m0-42.666666V87.226667c52.386667 9.293333 101.793333 52.666667 140.96 124.453333a502.986667 502.986667 0 0 1 29.153333 64.24c-52.853333 13.313333-110.4 21-170.113333 22.48z m-42.666667-211.18V298.4c-59.713333-1.48-117.26-9.166667-170.113333-22.48a502.986667 502.986667 0 0 1 29.153333-64.24C410.206667 139.886667 459.613333 96.52 512 87.226667z m0 253.846666V512H298.933333c1.72-69.426667 11.826667-135.933333 29.426667-195.533333 57.14 14.666667 119.3 23.08 183.64 24.606666zM298.933333 554.666667H512v170.926666c-64.34 1.526667-126.5 9.94-183.64 24.606667-17.6-59.6-27.693333-126.106667-29.426667-195.533333zM512 768.266667v211.173333c-52.386667-9.293333-101.793333-52.666667-140.96-124.453333a502.986667 502.986667 0 0 1-29.153333-64.24c52.853333-13.313333 110.4-21 170.113333-22.48z m42.666667 211.173333V768.266667c59.713333 1.48 117.26 9.166667 170.113333 22.48a502.986667 502.986667 0 0 1-29.153333 64.24c-39.166667 71.793333-88.573333 115.16-140.96 124.453333z m0-253.846667V554.666667h213.066666c-1.72 69.426667-11.826667 135.933333-29.426666 195.533333-57.14-14.666667-119.3-23.08-183.64-24.606667zM810.406667 512c-1.74-73.373333-12.406667-143.906667-31.153334-207.4 34.6-11.22 66.9-24.886667 96.166667-40.853333q5.586667-3.053333 10.993333-6.193334A445.866667 445.866667 0 0 1 980.826667 512zM858 224.626667c-1 0.553333-2 1.113333-3 1.666666-27.053333 14.753333-56.98 27.413333-89.1 37.826667a546.88 546.88 0 0 0-32.806667-72.873333c-18.113333-33.206667-38.526667-61.333333-60.926666-84A448.106667 448.106667 0 0 1 858 224.626667z m-463.473333-117.333334c-22.4 22.666667-42.813333 50.78-60.926667 84a546 546 0 0 0-32.806667 72.873334c-32.126667-10.46-62.06-23.12-89.113333-37.873334-1.013333-0.553333-2-1.113333-3-1.666666a448.106667 448.106667 0 0 1 185.833333-117.366667zM208.666667 842.04c1-0.553333 2-1.113333 3-1.666667 27.053333-14.753333 56.98-27.413333 89.1-37.826666a546 546 0 0 0 32.806666 72.873333c18.113333 33.206667 38.526667 61.333333 60.926667 84A448.106667 448.106667 0 0 1 208.666667 842.04z m463.473333 117.333333c22.4-22.666667 42.813333-50.78 60.926667-84a546.88 546.88 0 0 0 32.806666-72.873333c32.12 10.413333 62.046667 23.073333 89.1 37.826667 1.013333 0.553333 2 1.113333 3 1.666666a448.106667 448.106667 0 0 1-185.82 117.413334z" fill="#353436" p-id="7255"></path></svg>
|
After Width: | Height: | Size: 3.5 KiB |
1
app/icons/search_open.svg
Normal file
1
app/icons/search_open.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683102597441" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7012" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M191.246667 263.746667c28.546667 15.573333 60 28.96 93.606666 40-17.2 64.12-27.02 134.82-28.62 208.226666H43.133333a488.26 488.26 0 0 1 101.233334-277.766666 422.393333 422.393333 0 0 0 46.88 29.54zM512 554.666667H298.913333c1.566667 69.333333 10.78 136 26.853334 196.206666 57.846667-15.08 120.92-23.733333 186.233333-25.28z m-320.753333 248.253333c28.546667-15.573333 60-28.96 93.606666-40-17.2-64.12-27.02-134.82-28.62-208.226667H43.133333a488.26 488.26 0 0 0 101.233334 277.793334 422.393333 422.393333 0 0 1 46.88-29.566667zM554.666667 66v232.4c61.16-1.513333 120.053333-9.54 173.953333-23.453333a549.766667 549.766667 0 0 0-33.18-78.666667C656.246667 121.126667 606.906667 75.74 554.666667 66z m-42.666667 446V341.073333c-65.333333-1.546667-128.386667-10.2-186.233333-25.28C309.693333 376 300.48 442.666667 298.913333 512z m228.9-196.206667C683.053333 330.873333 620 339.526667 554.666667 341.073333V512h213.086666c-1.566667-69.333333-10.78-136-26.853333-196.206667zM512 1000.666667v-232.4c-61.16 1.513333-120.053333 9.54-173.953333 23.453333a549.766667 549.766667 0 0 0 33.18 78.7C410.42 945.54 459.76 990.926667 512 1000.666667zM371.226667 196.246667a549.766667 549.766667 0 0 0-33.18 78.7c53.9 13.913333 112.793333 21.94 173.953333 23.453333V66c-52.24 9.74-101.58 55.126667-140.773333 130.246667z m504.193333 67.5c-28.546667 15.573333-60 28.96-93.606667 40 17.2 64.12 27.02 134.82 28.62 208.226666h213.1a488.26 488.26 0 0 0-101.233333-277.793333 422.393333 422.393333 0 0 1-46.88 29.566667z m-663.74-37.453334c26 14.193333 54.7 26.44 85.433333 36.626667 10.293333-30.866667 22.426667-59.833333 36.286667-86.406667 24.866667-47.666667 54.086667-85.3 86.853333-111.846666Q428.666667 57.833333 437.28 52.073333c-104.086667 20.666667-196.313333 74.233333-265.38 149.426667a387.226667 387.226667 0 0 0 39.78 24.793333zM420.253333 1002c-32.766667-26.56-62-64.193333-86.853333-111.86-13.86-26.566667-26-55.54-36.286667-86.406667-30.733333 10.186667-59.413333 22.433333-85.433333 36.626667a387.226667 387.226667 0 0 0-39.78 24.793333c69.066667 75.193333 161.293333 128.766667 265.38 149.426667q-8.613333-5.746667-17.026667-12.58z m434.733334-161.64c-26-14.193333-54.666667-26.44-85.433334-36.626667-10.293333 30.866667-22.426667 59.84-36.286666 86.406667-24.866667 47.666667-54.086667 85.333333-86.853334 111.86q-8.42 6.82-17.026666 12.58c104.086667-20.666667 196.313333-74.233333 265.38-149.426667a387.226667 387.226667 0 0 0-39.78-24.78z m-159.546667 30.046667a549.766667 549.766667 0 0 0 33.18-78.7c-53.9-13.913333-112.793333-21.94-173.953333-23.453334V1000.666667c52.24-9.74 101.58-55.126667 140.773333-130.246667zM554.666667 554.666667v170.926666c65.333333 1.546667 128.386667 10.2 186.233333 25.28 16.073333-60.233333 25.286667-126.84 26.853333-196.206666zM646.413333 64.666667c32.766667 26.56 62 64.193333 86.853334 111.86 13.86 26.573333 26 55.54 36.286666 86.406666 30.733333-10.186667 59.413333-22.433333 85.433334-36.626666a387.226667 387.226667 0 0 0 39.78-24.793334c-69.066667-75.206667-161.293333-128.78-265.38-149.44Q638 57.833333 646.413333 64.666667z m275.886667 767.806666A488.26 488.26 0 0 0 1023.533333 554.666667h-213.1c-1.6 73.406667-11.42 144.106667-28.62 208.226666 33.633333 11.066667 65.06 24.453333 93.606667 40a422.393333 422.393333 0 0 1 46.88 29.566667z" fill="#353436" p-id="7013"></path></svg>
|
After Width: | Height: | Size: 3.6 KiB |
@ -73,9 +73,12 @@ const cn = {
|
||||
DisablePlugins: "关闭插件",
|
||||
UploadImage: "上传图片",
|
||||
UploadFle: "上传文件",
|
||||
OpenWebSearch: "开启联网",
|
||||
CloseWebSearch: "关闭联网",
|
||||
},
|
||||
Rename: "重命名对话",
|
||||
Typing: "正在输入…",
|
||||
Searching: "联网搜索中…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} 发送`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
|
@ -75,9 +75,12 @@ const en: LocaleType = {
|
||||
DisablePlugins: "Disable Plugins",
|
||||
UploadImage: "Upload Images",
|
||||
UploadFle: "Upload Files",
|
||||
OpenWebSearch: "Enable Web Search",
|
||||
CloseWebSearch: "Disable Web Search",
|
||||
},
|
||||
Rename: "Rename Chat",
|
||||
Typing: "Typing…",
|
||||
Searching: "Searching…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} to send`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
|
60
app/prompt.ts
Normal file
60
app/prompt.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export const MYFILES_BROWSER_TOOLS_SYSTEM_PROMPT = `
|
||||
# Tools
|
||||
|
||||
## myfiles_browser
|
||||
|
||||
You have the tool 'myfiles_browser' with the following functions:
|
||||
issues queries to search the file(s) uploaded in the current conversation and displays the results.
|
||||
|
||||
This tool is for browsing the files uploaded by the user.
|
||||
|
||||
Parts of the documents uploaded by users will be automatically included in the conversation. Only use this tool when the relevant parts don't contain the necessary information to fulfill the user's request.
|
||||
|
||||
If the user needs to summarize the document, they can summarize it through parts of the document.
|
||||
|
||||
Think carefully about how the information you find relates to the user's request. Respond as soon as you find information that clearly answers the request.
|
||||
|
||||
Issue multiple queries to the 'myfiles_browser' command only when the user's question needs to be decomposed to find different facts. In other scenarios, prefer providing a single query. Avoid single-word queries that are extremely broad and will return unrelated results.
|
||||
|
||||
Here are some examples of how to use the 'myfiles_browser' command:
|
||||
User: What was the GDP of France and Italy in the 1970s? => myfiles_browser(["france gdp 1970", "italy gdp 1970"])
|
||||
User: What does the report say about the GPT4 performance on MMLU? => myfiles_browser(["GPT4 MMLU performance"])
|
||||
User: How can I integrate customer relationship management system with third-party email marketing tools? => myfiles_browser(["customer management system marketing integration"])
|
||||
User: What are the best practices for data security and privacy for our cloud storage services? => myfiles_browser(["cloud storage security and privacy"])
|
||||
|
||||
The user has uploaded the following files:
|
||||
`;
|
||||
|
||||
export const WEB_SEARCH_ANSWER_ZH_PROMPT = `# 以下内容是基于用户发送的消息的搜索结果:
|
||||
{search_results}
|
||||
在我给你的搜索结果中,每个结果都是[webpage X begin]...[webpage X end]格式的,X代表每篇文章的数字索引。请在适当的情况下在句子末尾引用上下文。请按照引用编号[citation:X]的格式在答案中对应部分引用上下文。如果一句话源自多个上下文,请列出所有相关的引用编号,例如[citation:3][citation:5],切记不要将引用集中在最后返回引用编号,而是在答案对应部分列出。
|
||||
在回答时,请注意以下几点:
|
||||
- 今天是{cur_date}。
|
||||
- 并非搜索结果的所有内容都与用户的问题密切相关,你需要结合问题,对搜索结果进行甄别、筛选。
|
||||
- 对于列举类的问题(如列举所有航班信息),尽量将答案控制在10个要点以内,并告诉用户可以查看搜索来源、获得完整信息。优先提供信息完整、最相关的列举项;如非必要,不要主动告诉用户搜索结果未提供的内容。
|
||||
- 对于创作类的问题(如写论文),请务必在正文的段落中引用对应的参考编号,例如[citation:3][citation:5],不能只在文章末尾引用。你需要解读并概括用户的题目要求,选择合适的格式,充分利用搜索结果并抽取重要信息,生成符合用户要求、极具思想深度、富有创造力与专业性的答案。你的创作篇幅需要尽可能延长,对于每一个要点的论述要推测用户的意图,给出尽可能多角度的回答要点,且务必信息量大、论述详尽。
|
||||
- 如果回答很长,请尽量结构化、分段落总结。如果需要分点作答,尽量控制在5个点以内,并合并相关的内容。
|
||||
- 对于客观类的问答,如果问题的答案非常简短,可以适当补充一到两句相关信息,以丰富内容。
|
||||
- 你需要根据用户要求和回答内容选择合适、美观的回答格式,确保可读性强。
|
||||
- 你的回答应该综合多个相关网页来回答,不能重复引用一个网页。
|
||||
- 除非用户要求,否则你回答的语言需要和用户提问的语言保持一致。
|
||||
|
||||
# 用户消息为:
|
||||
{question}`;
|
||||
|
||||
export const WEB_SEARCH_ANSWER_EN_PROMPT = `# The following contents are the search results related to the user's message:
|
||||
{search_results}
|
||||
In the search results I provide to you, each result is formatted as [webpage X begin]...[webpage X end], where X represents the numerical index of each article. Please cite the context at the end of the relevant sentence when appropriate. Use the citation format [citation:X] in the corresponding part of your answer. If a sentence is derived from multiple contexts, list all relevant citation numbers, such as [citation:3][citation:5]. Be sure not to cluster all citations at the end; instead, include them in the corresponding parts of the answer.
|
||||
When responding, please keep the following points in mind:
|
||||
- Today is {cur_date}.
|
||||
- Not all content in the search results is closely related to the user's question. You need to evaluate and filter the search results based on the question.
|
||||
- For listing-type questions (e.g., listing all flight information), try to limit the answer to 10 key points and inform the user that they can refer to the search sources for complete information. Prioritize providing the most complete and relevant items in the list. Avoid mentioning content not provided in the search results unless necessary.
|
||||
- For creative tasks (e.g., writing an essay), ensure that references are cited within the body of the text, such as [citation:3][citation:5], rather than only at the end of the text. You need to interpret and summarize the user's requirements, choose an appropriate format, fully utilize the search results, extract key information, and generate an answer that is insightful, creative, and professional. Extend the length of your response as much as possible, addressing each point in detail and from multiple perspectives, ensuring the content is rich and thorough.
|
||||
- If the response is lengthy, structure it well and summarize it in paragraphs. If a point-by-point format is needed, try to limit it to 5 points and merge related content.
|
||||
- For objective Q&A, if the answer is very brief, you may add one or two related sentences to enrich the content.
|
||||
- Choose an appropriate and visually appealing format for your response based on the user's requirements and the content of the answer, ensuring strong readability.
|
||||
- Your answer should synthesize information from multiple relevant webpages and avoid repeatedly citing the same webpage.
|
||||
- Unless the user requests otherwise, your response should be in the same language as the user's question.
|
||||
|
||||
# The user's message is:
|
||||
{question}`;
|
@ -142,6 +142,7 @@ const DEFAULT_ACCESS_STATE = {
|
||||
defaultModel: "",
|
||||
visionModels: "",
|
||||
isEnableRAG: false,
|
||||
isEnableWebSearch: false,
|
||||
|
||||
// tts config
|
||||
edgeTTSVoiceName: "zh-CN-YunxiNeural",
|
||||
@ -191,6 +192,12 @@ export const useAccessStore = createPersistStore(
|
||||
return get().isEnableRAG;
|
||||
},
|
||||
|
||||
enableWebSearch() {
|
||||
this.fetch();
|
||||
|
||||
return get().isEnableWebSearch;
|
||||
},
|
||||
|
||||
isValidOpenAI() {
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
|
@ -33,8 +33,9 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { FileInfo } from "../client/platforms/utils";
|
||||
import { FileInfo, WebApi } from "../client/platforms/utils";
|
||||
import { usePluginStore } from "./plugin";
|
||||
import { TavilySearchResponse } from "@tavily/core";
|
||||
|
||||
export interface ChatToolMessage {
|
||||
toolName: string;
|
||||
@ -64,7 +65,7 @@ export type ChatMessage = RequestMessage & {
|
||||
id: string;
|
||||
model?: ModelType;
|
||||
tools?: ChatMessageTool[];
|
||||
audio_url?: string;
|
||||
audioUrl?: string;
|
||||
};
|
||||
|
||||
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
||||
@ -383,9 +384,11 @@ export const useChatStore = createPersistStore(
|
||||
content: string,
|
||||
attachImages?: string[],
|
||||
attachFiles?: FileInfo[],
|
||||
webSearchReference?: TavilySearchResponse,
|
||||
) {
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const userContent = fillTemplateWith(content, modelConfig);
|
||||
console.log("[User Input] after template: ", userContent);
|
||||
@ -413,6 +416,7 @@ export const useChatStore = createPersistStore(
|
||||
role: "user",
|
||||
content: mContent,
|
||||
fileInfos: attachFiles,
|
||||
webSearchReferences: webSearchReference,
|
||||
});
|
||||
|
||||
const botMessage: ChatMessage = createMessage({
|
||||
@ -534,6 +538,16 @@ export const useChatStore = createPersistStore(
|
||||
};
|
||||
agentCall();
|
||||
} else {
|
||||
if (session.mask.webSearch && accessStore.enableWebSearch()) {
|
||||
botMessage.content = Locale.Chat.Searching;
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
const webApi = new WebApi();
|
||||
const webSearchReference = await webApi.search(content);
|
||||
userMessage.webSearchReferences = webSearchReference;
|
||||
botMessage.webSearchReferences = webSearchReference;
|
||||
}
|
||||
// make request
|
||||
api.llm.chat({
|
||||
messages: sendMessages,
|
||||
|
@ -18,6 +18,7 @@ export type Mask = {
|
||||
lang: Lang;
|
||||
builtin: boolean;
|
||||
usePlugins?: boolean;
|
||||
webSearch?: boolean;
|
||||
// 上游插件业务参数
|
||||
plugin?: string[];
|
||||
enableArtifacts?: boolean;
|
||||
|
36
app/utils.ts
36
app/utils.ts
@ -1,11 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
import Locale, { getLang } from "./locales";
|
||||
import { RequestMessage } from "./client/api";
|
||||
import { DEFAULT_MODELS } from "./constant";
|
||||
import { ServiceProvider } from "./constant";
|
||||
// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
|
||||
import { fetch as tauriStreamFetch } from "./utils/stream";
|
||||
import {
|
||||
WEB_SEARCH_ANSWER_EN_PROMPT,
|
||||
WEB_SEARCH_ANSWER_ZH_PROMPT,
|
||||
} from "./prompt";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
// Fix an issue where double quotes still show in the Indonesian language
|
||||
@ -227,6 +231,36 @@ export function isMacOS(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getWebReferenceMessageTextContent(message: RequestMessage) {
|
||||
let prompt = getMessageTextContent(message);
|
||||
if (
|
||||
message.webSearchReferences &&
|
||||
message.webSearchReferences.results.length > 0
|
||||
) {
|
||||
const searchResults = message.webSearchReferences.results
|
||||
.map((result, index) => {
|
||||
return `[webpage ${index + 1} begin]
|
||||
[webpage title]${result.title}
|
||||
[webpage url]${result.url}
|
||||
[webpage content begin]
|
||||
${result.content}
|
||||
[webpage content end]
|
||||
[webpage ${index + 1} end]
|
||||
`;
|
||||
})
|
||||
.join("\n");
|
||||
const isZh = getLang() == "cn";
|
||||
const promptTemplate = isZh
|
||||
? WEB_SEARCH_ANSWER_ZH_PROMPT
|
||||
: WEB_SEARCH_ANSWER_EN_PROMPT;
|
||||
prompt = promptTemplate
|
||||
.replace("{cur_date}", new Date().toLocaleString())
|
||||
.replace("{search_results}", searchResults)
|
||||
.replace("{question}", prompt);
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
export function getMessageTextContent(message: RequestMessage) {
|
||||
if (typeof message.content === "string") {
|
||||
return message.content;
|
||||
|
@ -34,6 +34,7 @@
|
||||
"@qdrant/js-client-rest": "^1.8.2",
|
||||
"@supabase/supabase-js": "^2.44.2",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@tavily/core": "^0.3.1",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"axios": "^1.7.5",
|
||||
@ -109,12 +110,12 @@
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.16.0",
|
||||
"typescript": "5.2.2",
|
||||
"watch": "^1.0.2",
|
||||
"webpack": "^5.88.1",
|
||||
"raw-loader": "^4.0.2"
|
||||
"webpack": "^5.88.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"lint-staged/yaml": "^2.2.2",
|
||||
|
17
yarn.lock
17
yarn.lock
@ -3430,6 +3430,14 @@
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.5.11"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.5.11"
|
||||
|
||||
"@tavily/core@^0.3.1":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tavily/core/-/core-0.3.1.tgz#bd07c7328be76b061a3dc12e6b978b6ca3a56627"
|
||||
integrity sha512-7jyvPWG4Zjst0s4v0FMLO1f/dfHqs4FnqvKm86zOGYzXxSfxHu0isbLzlwjJad0csYwF0kifdlECTuNouHfr5A==
|
||||
dependencies:
|
||||
axios "^1.7.7"
|
||||
js-tiktoken "^1.0.14"
|
||||
|
||||
"@testing-library/dom@^10.4.0":
|
||||
version "10.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8"
|
||||
@ -4323,7 +4331,7 @@ axe-core@^4.9.1:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae"
|
||||
integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==
|
||||
|
||||
axios@^1.7.5:
|
||||
axios@^1.7.5, axios@^1.7.7:
|
||||
version "1.7.9"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
|
||||
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
|
||||
@ -7876,6 +7884,13 @@ js-tiktoken@^1.0.12:
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
js-tiktoken@^1.0.14:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.19.tgz#0298b584382f1d47d4b45cb93d382f11780eab78"
|
||||
integrity sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
Loading…
Reference in New Issue
Block a user