Compare commits

...

27 Commits

Author SHA1 Message Date
Shay Molcho
43d25312f0 Merge b05b2e78cd into 11b37c15bd 2025-04-17 13:03:18 +08:00
RiverRay
11b37c15bd Merge pull request #6450 from stephen-zeng/main
Some checks failed
Run Tests / test (push) Has been cancelled
Add gpt-4.1 family & gpt-4.5-preview support
2025-04-17 08:29:19 +08:00
QwQwQ
1d0038f17d add gpt-4.5-preview support 2025-04-16 22:10:47 +08:00
QwQwQ
619fa519c0 add gpt-4.1 family support 2025-04-16 22:02:35 +08:00
RiverRay
48469bd8ca Merge pull request #6392 from ChatGPTNextWeb/Leizhenpeng-patch-6
Some checks failed
Run Tests / test (push) Has been cancelled
Update README.md
2025-03-20 17:52:02 +08:00
RiverRay
5a5e887f2b Update README.md 2025-03-20 17:51:47 +08:00
RiverRay
b6f5d75656 Merge pull request #6344 from vangie/fix/jest-setup-esm
Some checks failed
Run Tests / test (push) Has been cancelled
test: fix unit test failures
2025-03-14 20:04:56 +08:00
Vangie Du
0d41a17ef6 test: fix unit test failures 2025-03-07 14:49:17 +08:00
RiverRay
f7cde17919 Merge pull request #6292 from Little-LittleProgrammer/feature/alibaba-omni-support
Some checks failed
Run Tests / test (push) Has been cancelled
feat(alibaba): Added alibaba vision model and omni model support
2025-03-01 10:25:16 +08:00
RiverRay
570cbb34b6 Merge pull request #6310 from agi-dude/patch-1
Remove duplicate links
2025-03-01 10:24:38 +08:00
RiverRay
7aa9ae0a3e Merge pull request #6311 from ChatGPTNextWeb/6305-bugthe-first-message-except-the-system-message-of-deepseek-reasoner-must-be-a-user-message-but-an-assistant-message-detected
Some checks are pending
Run Tests / test (push) Waiting to run
fix: enforce that the first message (excluding system messages) is a …
2025-02-28 19:48:09 +08:00
Kadxy
2d4180f5be fix: update request payload to use filtered messages in Deepseek API 2025-02-28 13:59:30 +08:00
Kadxy
9f0182b55e fix: enforce that the first message (excluding system messages) is a user message in the Deepseek API 2025-02-28 13:54:58 +08:00
Mr. AGI
ad6666eeaf Update README.md 2025-02-28 10:47:52 +05:00
EvanWu
a2c4e468a0 fix(app/utils/chat.ts): fix type error 2025-02-26 19:58:32 +08:00
RiverRay
2167076652 Merge pull request #6293 from hyiip/main
Some checks failed
Run Tests / test (push) Has been cancelled
claude 3.7 support
2025-02-26 18:41:28 +08:00
RiverRay
e123076250 Merge pull request #6295 from rexkyng/patch-1
Fix: Improve Mistral icon detection and remove redundant code.
2025-02-26 18:39:59 +08:00
Rex Ng
ebcb4db245 Fix: Improve Mistral icon detection and remove redundant code.
- Added "codestral" to the list of acceptable names for the Mistral icon, ensuring proper detection.
- Removed duplicate `toLowerCase()` calls.
2025-02-25 14:30:18 +08:00
EvanWu
0a25a1a8cb refacto(app/utils/chat.ts)r: optimize function preProcessImageContentBase 2025-02-25 09:22:47 +08:00
hyiip
f3154b20a5 claude 3.7 support 2025-02-25 03:55:24 +08:00
EvanWu
b709ee3983 feat(alibaba): Added alibaba vision model and omni model support 2025-02-24 20:18:07 +08:00
RiverRay
f5f3ce94f6 Update README.md
Some checks failed
Run Tests / test (push) Has been cancelled
2025-02-21 08:56:43 +08:00
RiverRay
2b5f600308 Update README.md 2025-02-21 08:55:40 +08:00
RiverRay
b966107117 Merge pull request #6235 from DBCDK/danish-locale
Some checks failed
Run Tests / test (push) Has been cancelled
Translation to danish
2025-02-17 22:58:01 +08:00
Rasmus Erik Voel Jensen
90827fc593 danish rewording / improved button label 2025-02-15 13:08:58 +01:00
Rasmus Erik Voel Jensen
008e339b6d danish locale 2025-02-15 12:52:44 +01:00
Shay Molcho
b05b2e78cd Added missing periods (.) in multiple places
This commit adds missing periods (.) in several places to ensure consistency and improve readability in the code and documentation
2025-01-22 20:23:51 +02:00
13 changed files with 954 additions and 35 deletions

View File

@@ -7,7 +7,7 @@
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
<h1 align="center">NextChat</h1>
English / [简体中文](./README_CN.md)
@@ -22,7 +22,6 @@ English / [简体中文](./README_CN.md)
[![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url]
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
[NextChatAI](https://nextchat.club?utm_source=readme) / [Web App Demo](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
@@ -112,7 +111,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.14.0 Now supports Artifacts & SD.
- 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now.
- 🚀 v2.8 now we have a client that runs across all platforms!
@@ -320,7 +319,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
### `DEFAULT_MODEL` optional
Change default model
Change default model.
### `VISION_MODELS` (optional)
@@ -351,7 +350,7 @@ Customize Stability API url.
### `ENABLE_MCP` (optional)
Enable MCPModel Context ProtocolFeature
Enable MCPModel Context ProtocolFeature.
### `SILICONFLOW_API_KEY` (optional)

View File

@@ -40,6 +40,11 @@ export interface MultimodalContent {
};
}
export interface MultimodalContentForAlibaba {
text?: string;
image?: string;
}
export interface RequestMessage {
role: MessageRole;
content: string | MultimodalContent[];

View File

@@ -7,7 +7,10 @@ import {
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { streamWithThink } from "@/app/utils/chat";
import {
preProcessImageContentForAlibabaDashScope,
streamWithThink,
} from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
@@ -15,12 +18,14 @@ import {
LLMModel,
SpeechOptions,
MultimodalContent,
MultimodalContentForAlibaba,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import {
getMessageTextContent,
getMessageTextContentWithoutThinking,
getTimeoutMSByModel,
isVisionModel,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
@@ -89,14 +94,6 @@ export class QwenApi implements LLMApi {
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
content:
v.role === "assistant"
? getMessageTextContentWithoutThinking(v)
: getMessageTextContent(v),
}));
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
@@ -105,6 +102,21 @@ export class QwenApi implements LLMApi {
},
};
const visionModel = isVisionModel(options.config.model);
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = (
visionModel
? await preProcessImageContentForAlibabaDashScope(v.content)
: v.role === "assistant"
? getMessageTextContentWithoutThinking(v)
: getMessageTextContent(v)
) as any;
messages.push({ role: v.role, content });
}
const shouldStream = !!options.config.stream;
const requestPayload: RequestPayload = {
model: modelConfig.model,
@@ -129,7 +141,7 @@ export class QwenApi implements LLMApi {
"X-DashScope-SSE": shouldStream ? "enable" : "disable",
};
const chatPath = this.path(Alibaba.ChatPath);
const chatPath = this.path(Alibaba.ChatPath(modelConfig.model));
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
@@ -162,7 +174,7 @@ export class QwenApi implements LLMApi {
const json = JSON.parse(text);
const choices = json.output.choices as Array<{
message: {
content: string | null;
content: string | null | MultimodalContentForAlibaba[];
tool_calls: ChatMessageTool[];
reasoning_content: string | null;
};
@@ -212,7 +224,9 @@ export class QwenApi implements LLMApi {
} else if (content && content.length > 0) {
return {
isThinking: false,
content: content,
content: Array.isArray(content)
? content.map((item) => item.text).join(",")
: content,
};
}

View File

@@ -75,6 +75,25 @@ export class DeepSeekApi implements LLMApi {
}
}
// 检测并修复消息顺序确保除system外的第一个消息是user
const filteredMessages: ChatOptions["messages"] = [];
let hasFoundFirstUser = false;
for (const msg of messages) {
if (msg.role === "system") {
// Keep all system messages
filteredMessages.push(msg);
} else if (msg.role === "user") {
// User message directly added
filteredMessages.push(msg);
hasFoundFirstUser = true;
} else if (hasFoundFirstUser) {
// After finding the first user message, all subsequent non-system messages are retained.
filteredMessages.push(msg);
}
// If hasFoundFirstUser is false and it is not a system message, it will be skipped.
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
@@ -85,7 +104,7 @@ export class DeepSeekApi implements LLMApi {
};
const requestPayload: RequestPayload = {
messages,
messages: filteredMessages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,

View File

@@ -66,11 +66,11 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
LlmIcon = BotIconGemma;
} else if (modelName.startsWith("claude")) {
LlmIcon = BotIconClaude;
} else if (modelName.toLowerCase().includes("llama")) {
} else if (modelName.includes("llama")) {
LlmIcon = BotIconMeta;
} else if (modelName.startsWith("mixtral")) {
} else if (modelName.startsWith("mixtral") || modelName.startsWith("codestral")) {
LlmIcon = BotIconMistral;
} else if (modelName.toLowerCase().includes("deepseek")) {
} else if (modelName.includes("deepseek")) {
LlmIcon = BotIconDeepseek;
} else if (modelName.startsWith("moonshot")) {
LlmIcon = BotIconMoonshot;
@@ -85,7 +85,7 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
} else if (modelName.startsWith("doubao") || modelName.startsWith("ep-")) {
LlmIcon = BotIconDoubao;
} else if (
modelName.toLowerCase().includes("glm") ||
modelName.includes("glm") ||
modelName.startsWith("cogview-") ||
modelName.startsWith("cogvideox-")
) {

View File

@@ -221,7 +221,12 @@ export const ByteDance = {
export const Alibaba = {
ExampleEndpoint: ALIBABA_BASE_URL,
ChatPath: "v1/services/aigc/text-generation/generation",
ChatPath: (modelName: string) => {
if (modelName.includes("vl") || modelName.includes("omni")) {
return "v1/services/aigc/multimodal-generation/generation";
}
return `v1/services/aigc/text-generation/generation`;
},
};
export const Tencent = {
@@ -412,6 +417,14 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4-turbo": "2023-12",
"gpt-4-turbo-2024-04-09": "2023-12",
"gpt-4-turbo-preview": "2023-12",
"gpt-4.1": "2024-06",
"gpt-4.1-2025-04-14": "2024-06",
"gpt-4.1-mini": "2024-06",
"gpt-4.1-mini-2025-04-14": "2024-06",
"gpt-4.1-nano": "2024-06",
"gpt-4.1-nano-2025-04-14": "2024-06",
"gpt-4.5-preview": "2023-10",
"gpt-4.5-preview-2025-02-27": "2023-10",
"gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10",
@@ -453,6 +466,7 @@ export const DEFAULT_TTS_VOICES = [
export const VISION_MODEL_REGEXES = [
/vision/,
/gpt-4o/,
/gpt-4\.1/,
/claude-3/,
/gemini-1\.5/,
/gemini-exp/,
@@ -480,6 +494,14 @@ const openaiModels = [
"gpt-4-32k-0613",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4.1",
"gpt-4.1-2025-04-14",
"gpt-4.1-mini",
"gpt-4.1-mini-2025-04-14",
"gpt-4.1-nano",
"gpt-4.1-nano-2025-04-14",
"gpt-4.5-preview",
"gpt-4.5-preview-2025-02-27",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
@@ -535,6 +557,8 @@ const anthropicModels = [
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
];
const baiduModels = [
@@ -568,6 +592,9 @@ const alibabaModes = [
"qwen-max-0403",
"qwen-max-0107",
"qwen-max-longcontext",
"qwen-omni-turbo",
"qwen-vl-plus",
"qwen-vl-max",
];
const tencentModels = [

832
app/locales/da.ts Normal file
View File

@@ -0,0 +1,832 @@
import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
import { PartialLocaleType } from "./index";
const isApp = !!getClientConfig()?.isApp;
const da: PartialLocaleType = {
WIP: "Der kommer snart mere...",
Error: {
Unauthorized: isApp
? `Hov, der skete en fejl. Sådan kan du komme videre:
\\ 1⃣ Er du ny her? [Tryk for at starte nu 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Vil du bruge dine egne OpenAI-nøgler? [Tryk her](/#/settings) for at ændre indstillinger ⚙️`
: `Hov, der skete en fejl. Lad os løse det:
\\ 1⃣ Er du ny her? [Tryk for at starte nu 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Bruger du en privat opsætning? [Tryk her](/#/auth) for at taste din nøgle 🔑
\\ 3⃣ Vil du bruge dine egne OpenAI-nøgler? [Tryk her](/#/settings) for at ændre indstillinger ⚙️
`,
},
Auth: {
Return: "Tilbage",
Title: "Adgangskode",
Tips: "Skriv venligst koden herunder",
SubTips: "Eller brug din egen OpenAI- eller Google-nøgle",
Input: "Adgangskode",
Confirm: "OK",
Later: "Senere",
SaasTips: "Hvis det er for svært, kan du starte nu",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} beskeder`,
},
Chat: {
SubTitle: (count: number) => `${count} beskeder`,
EditMessage: {
Title: "Rediger beskeder",
Topic: {
Title: "Emne",
SubTitle: "Skift emne for denne chat",
},
},
Actions: {
ChatList: "Gå til chatliste",
CompressedHistory: "Komprimeret historie",
Export: "Eksporter alle beskeder som Markdown",
Copy: "Kopiér",
Stop: "Stop",
Retry: "Prøv igen",
Pin: "Fastgør",
PinToastContent: "1 besked er nu fastgjort",
PinToastAction: "Se",
Delete: "Slet",
Edit: "Rediger",
FullScreen: "Fuld skærm",
RefreshTitle: "Opdatér titel",
RefreshToast: "Anmodning om ny titel sendt",
Speech: "Afspil",
StopSpeech: "Stop",
},
Commands: {
new: "Ny chat",
newm: "Ny chat med persona",
next: "Næste chat",
prev: "Forrige chat",
clear: "Ryd alt før",
fork: "Kopiér chat",
del: "Slet chat",
},
InputActions: {
Stop: "Stop",
ToBottom: "Ned til nyeste",
Theme: {
auto: "Automatisk",
light: "Lyst tema",
dark: "Mørkt tema",
},
Prompt: "Prompts",
Masks: "Personaer",
Clear: "Ryd kontekst",
Settings: "Indstillinger",
UploadImage: "Upload billeder",
},
Rename: "Omdøb chat",
Typing: "Skriver…",
Input: (submitKey: string) => {
let inputHints = `${submitKey} for at sende`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter for ny linje";
}
return (
inputHints + ", / for at søge i prompts, : for at bruge kommandoer"
);
},
Send: "Send",
StartSpeak: "Start oplæsning",
StopSpeak: "Stop oplæsning",
Config: {
Reset: "Nulstil til standard",
SaveAs: "Gem som persona",
},
IsContext: "Ekstra prompt til baggrund",
ShortcutKey: {
Title: "Hurtigtaster",
newChat: "Åbn ny chat",
focusInput: "Fokus på tekstfeltet",
copyLastMessage: "Kopiér sidste svar",
copyLastCode: "Kopiér sidste kodeblok",
showShortcutKey: "Vis hurtigtaster",
clearContext: "Ryd kontekst",
},
},
Export: {
Title: "Eksportér beskeder",
Copy: "Kopiér alt",
Download: "Download",
MessageFromYou: "Fra dig",
MessageFromChatGPT: "Fra ChatGPT",
Share: "Del til ShareGPT",
Format: {
Title: "Filformat",
SubTitle: "Vælg enten Markdown eller PNG-billede",
},
IncludeContext: {
Title: "Tag baggrund med",
SubTitle: "Skal ekstra baggrund (persona) med i eksporten?",
},
Steps: {
Select: "Vælg",
Preview: "Forhåndsvis",
},
Image: {
Toast: "Laver billede...",
Modal: "Tryk længe eller højreklik for at gemme",
},
Artifacts: {
Title: "Del side",
Error: "Fejl ved deling",
},
},
Select: {
Search: "Søg",
All: "Vælg alle",
Latest: "Vælg nyeste",
Clear: "Ryd alt",
},
Memory: {
Title: "Huskesætning",
EmptyContent: "Ingenting lige nu.",
Send: "Send huskesætning",
Copy: "Kopiér huskesætning",
Reset: "Nulstil chat",
ResetConfirm:
"Dette sletter nuværende samtale og hukommelse. Er du sikker?",
},
Home: {
NewChat: "Ny Chat",
DeleteChat: "Vil du slette den valgte chat?",
DeleteToast: "Chat slettet",
Revert: "Fortryd",
},
Settings: {
Title: "Indstillinger",
SubTitle: "Alle indstillinger",
ShowPassword: "Vis kodeord",
Danger: {
Reset: {
Title: "Nulstil alle indstillinger",
SubTitle: "Gendan alt til standard",
Action: "Nulstil",
Confirm: "Vil du virkelig nulstille alt?",
},
Clear: {
Title: "Slet alle data",
SubTitle: "Sletter alt om beskeder og indstillinger",
Action: "Slet",
Confirm: "Er du sikker på, at du vil slette alt?",
},
},
Lang: {
Name: "Language",
All: "Alle sprog",
},
Avatar: "Avatar",
FontSize: {
Title: "Skriftstørrelse",
SubTitle: "Vælg, hvor stor teksten skal være",
},
FontFamily: {
Title: "Skrifttype",
SubTitle: "Hvis tom, bruger den standard skrifttype",
Placeholder: "Skrifttype-navn",
},
InjectSystemPrompts: {
Title: "Tilføj system-prompt",
SubTitle: "Læg altid en ekstra prompt først i anmodninger",
},
InputTemplate: {
Title: "Tekstskabelon",
SubTitle: "Den seneste besked placeres i denne skabelon",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Du har nyeste version",
CheckUpdate: "Tjek efter opdatering",
IsChecking: "Tjekker...",
FoundUpdate: (x: string) => `Ny version fundet: ${x}`,
GoToUpdate: "Opdatér",
Success: "Opdatering lykkedes.",
Failed: "Opdatering mislykkedes.",
},
SendKey: "Tast for send",
Theme: "Tema",
TightBorder: "Stram kant",
SendPreviewBubble: {
Title: "Forhåndsvisnings-boble",
SubTitle: "Vis tekst, før den sendes",
},
AutoGenerateTitle: {
Title: "Lav titel automatisk",
SubTitle: "Foreslå en titel ud fra chatten",
},
Sync: {
CloudState: "Seneste opdatering",
NotSyncYet: "Endnu ikke synkroniseret",
Success: "Synkronisering lykkedes",
Fail: "Synkronisering mislykkedes",
Config: {
Modal: {
Title: "Indstil synk",
Check: "Tjek forbindelse",
},
SyncType: {
Title: "Synk-type",
SubTitle: "Vælg en synk-tjeneste",
},
Proxy: {
Title: "Aktivér proxy",
SubTitle: "Brug proxy for at undgå netværksproblemer",
},
ProxyUrl: {
Title: "Proxy-adresse",
SubTitle: "Bruges kun til projektets egen proxy",
},
WebDav: {
Endpoint: "WebDAV-adresse",
UserName: "Brugernavn",
Password: "Kodeord",
},
UpStash: {
Endpoint: "UpStash Redis REST URL",
UserName: "Backup-navn",
Password: "UpStash Redis REST Token",
},
},
LocalState: "Lokale data",
Overview: (overview: any) =>
`${overview.chat} chats, ${overview.message} beskeder, ${overview.prompt} prompts, ${overview.mask} personaer`,
ImportFailed: "Import mislykkedes",
},
Mask: {
Splash: {
Title: "Persona-forside",
SubTitle: "Vis denne side, når du opretter ny chat",
},
Builtin: {
Title: "Skjul indbyggede personaer",
SubTitle: "Vis ikke de indbyggede personaer i listen",
},
},
Prompt: {
Disable: {
Title: "Slå auto-forslag fra",
SubTitle: "Tast / for at få forslag",
},
List: "Prompt-liste",
ListCount: (builtin: number, custom: number) =>
`${builtin} indbygget, ${custom} brugerdefineret`,
Edit: "Rediger",
Modal: {
Title: "Prompt-liste",
Add: "Tilføj",
Search: "Søg prompts",
},
EditModal: {
Title: "Rediger prompt",
},
},
HistoryCount: {
Title: "Antal beskeder, der følger med",
SubTitle: "Hvor mange af de tidligere beskeder, der sendes hver gang",
},
CompressThreshold: {
Title: "Komprimeringsgrænse",
SubTitle:
"Hvis chatten bliver for lang, vil den komprimeres efter dette antal tegn",
},
Usage: {
Title: "Brug og saldo",
SubTitle(used: any, total: any) {
return `Du har brugt $${used} i denne måned, og din grænse er $${total}.`;
},
IsChecking: "Tjekker...",
Check: "Tjek igen",
NoAccess: "Indtast API-nøgle for at se forbrug",
},
Access: {
AccessCode: {
Title: "Adgangskode",
SubTitle: "Adgangskontrol er slået til",
Placeholder: "Skriv kode her",
},
CustomEndpoint: {
Title: "Brugerdefineret adresse",
SubTitle: "Brug Azure eller OpenAI fra egen server",
},
Provider: {
Title: "Model-udbyder",
SubTitle: "Vælg Azure eller OpenAI",
},
OpenAI: {
ApiKey: {
Title: "OpenAI API-nøgle",
SubTitle: "Brug din egen nøgle",
Placeholder: "sk-xxx",
},
Endpoint: {
Title: "OpenAI Endpoint",
SubTitle: "Skal starte med http(s):// eller /api/openai som standard",
},
},
Azure: {
ApiKey: {
Title: "Azure Api Key",
SubTitle: "Hent din nøgle fra Azure-portalen",
Placeholder: "Azure Api Key",
},
Endpoint: {
Title: "Azure Endpoint",
SubTitle: "F.eks.: ",
},
ApiVerion: {
Title: "Azure Api Version",
SubTitle: "Hentet fra Azure-portalen",
},
},
Anthropic: {
ApiKey: {
Title: "Anthropic API-nøgle",
SubTitle: "Brug din egen Anthropic-nøgle",
Placeholder: "Anthropic API Key",
},
Endpoint: {
Title: "Endpoint-adresse",
SubTitle: "F.eks.: ",
},
ApiVerion: {
Title: "API-version (Claude)",
SubTitle: "Vælg den ønskede version",
},
},
Baidu: {
ApiKey: {
Title: "Baidu-nøgle",
SubTitle: "Din egen Baidu-nøgle",
Placeholder: "Baidu API Key",
},
SecretKey: {
Title: "Baidu hemmelig nøgle",
SubTitle: "Din egen hemmelige nøgle fra Baidu",
Placeholder: "Baidu Secret Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "Kan ikke ændres, se .env",
},
},
Tencent: {
ApiKey: {
Title: "Tencent-nøgle",
SubTitle: "Din egen nøgle fra Tencent",
Placeholder: "Tencent API Key",
},
SecretKey: {
Title: "Tencent hemmelig nøgle",
SubTitle: "Din egen hemmelige nøgle fra Tencent",
Placeholder: "Tencent Secret Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "Kan ikke ændres, se .env",
},
},
ByteDance: {
ApiKey: {
Title: "ByteDance-nøgle",
SubTitle: "Din egen nøgle til ByteDance",
Placeholder: "ByteDance API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
Alibaba: {
ApiKey: {
Title: "Alibaba-nøgle",
SubTitle: "Din egen Alibaba Cloud-nøgle",
Placeholder: "Alibaba Cloud API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
Moonshot: {
ApiKey: {
Title: "Moonshot-nøgle",
SubTitle: "Din egen Moonshot-nøgle",
Placeholder: "Moonshot API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
DeepSeek: {
ApiKey: {
Title: "DeepSeek-nøgle",
SubTitle: "Din egen DeepSeek-nøgle",
Placeholder: "DeepSeek API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
XAI: {
ApiKey: {
Title: "XAI-nøgle",
SubTitle: "Din egen XAI-nøgle",
Placeholder: "XAI API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
ChatGLM: {
ApiKey: {
Title: "ChatGLM-nøgle",
SubTitle: "Din egen ChatGLM-nøgle",
Placeholder: "ChatGLM API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
SiliconFlow: {
ApiKey: {
Title: "SiliconFlow-nøgle",
SubTitle: "Din egen SiliconFlow-nøgle",
Placeholder: "SiliconFlow API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
Stability: {
ApiKey: {
Title: "Stability-nøgle",
SubTitle: "Din egen Stability-nøgle",
Placeholder: "Stability API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
Iflytek: {
ApiKey: {
Title: "Iflytek API Key",
SubTitle: "Nøgle fra Iflytek",
Placeholder: "Iflytek API Key",
},
ApiSecret: {
Title: "Iflytek hemmelig nøgle",
SubTitle: "Hentet fra Iflytek",
Placeholder: "Iflytek API Secret",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
},
CustomModel: {
Title: "Egne modelnavne",
SubTitle: "Skriv komma-adskilte navne",
},
Google: {
ApiKey: {
Title: "Google-nøgle",
SubTitle: "Få din nøgle hos Google AI",
Placeholder: "Google AI API Key",
},
Endpoint: {
Title: "Adresse",
SubTitle: "F.eks.: ",
},
ApiVersion: {
Title: "API-version (til gemini-pro)",
SubTitle: "Vælg en bestemt version",
},
GoogleSafetySettings: {
Title: "Google sikkerhedsindstillinger",
SubTitle: "Vælg et niveau for indholdskontrol",
},
},
},
Model: "Model",
CompressModel: {
Title: "Opsummeringsmodel",
SubTitle: "Bruges til at korte historik ned og lave titel",
},
Temperature: {
Title: "Temperatur",
SubTitle: "Jo højere tal, jo mere kreativt svar",
},
TopP: {
Title: "Top P",
SubTitle: "Skal ikke ændres sammen med temperatur",
},
MaxTokens: {
Title: "Maks. længde",
SubTitle: "Hvor mange tokens (ord/stykker tekst) der kan bruges",
},
PresencePenalty: {
Title: "Nye emner",
SubTitle: "Jo højere tal, jo mere nyt indhold",
},
FrequencyPenalty: {
Title: "Gentagelsesstraf",
SubTitle: "Jo højere tal, jo mindre gentagelse",
},
TTS: {
Enable: {
Title: "Tænd for oplæsning (TTS)",
SubTitle: "Slå tekst-til-tale til",
},
Autoplay: {
Title: "Automatisk oplæsning",
SubTitle: "Laver lyd automatisk, hvis TTS er slået til",
},
Model: "Model",
Voice: {
Title: "Stemme",
SubTitle: "Hvilken stemme der bruges til lyd",
},
Speed: {
Title: "Hastighed",
SubTitle: "Hvor hurtigt der oplæses",
},
Engine: "TTS-motor",
},
Realtime: {
Enable: {
Title: "Live-chat",
SubTitle: "Slå live-svar til",
},
Provider: {
Title: "Modeludbyder",
SubTitle: "Vælg forskellig udbyder",
},
Model: {
Title: "Model",
SubTitle: "Vælg en model",
},
ApiKey: {
Title: "API-nøgle",
SubTitle: "Din nøgle",
Placeholder: "API-nøgle",
},
Azure: {
Endpoint: {
Title: "Adresse",
SubTitle: "Endpoint til Azure",
},
Deployment: {
Title: "Udrulningsnavn",
SubTitle: "Navn for dit Azure-setup",
},
},
Temperature: {
Title: "Temperatur",
SubTitle: "Højere tal = mere varierede svar",
},
},
},
Store: {
DefaultTopic: "Ny samtale",
BotHello: "Hej! Hvordan kan jeg hjælpe dig i dag?",
Error: "Noget gik galt. Prøv igen senere.",
Prompt: {
History: (content: string) =>
"Her er et kort resume af, hvad vi har snakket om: " + content,
Topic:
"Find en kort overskrift med 4-5 ord om emnet. Ingen tegnsætning eller anførselstegn.",
Summarize:
"Skriv et kort resumé (under 200 ord) af vores samtale til senere brug.",
},
},
Copy: {
Success: "Kopieret",
Failed: "Kunne ikke kopiere. Giv adgang til udklipsholder.",
},
Download: {
Success: "Filen er downloadet.",
Failed: "Download fejlede.",
},
Context: {
Toast: (x: any) => `Inkluderer ${x} ekstra prompts`,
Edit: "Chatindstillinger",
Add: "Tilføj prompt",
Clear: "Kontekst ryddet",
Revert: "Fortryd",
},
Discovery: {
Name: "Søgning og plugins",
},
Mcp: {
Name: "MCP",
},
FineTuned: {
Sysmessage: "Du er en hjælper, der skal...",
},
SearchChat: {
Name: "Søg",
Page: {
Title: "Søg i tidligere chats",
Search: "Skriv her for at søge",
NoResult: "Ingen resultater",
NoData: "Ingen data",
Loading: "Henter...",
SubTitle: (count: number) => `Fandt ${count} resultater`,
},
Item: {
View: "Vis",
},
},
Plugin: {
Name: "Plugin",
Page: {
Title: "Plugins",
SubTitle: (count: number) => `${count} plugins`,
Search: "Søg plugin",
Create: "Opret nyt",
Find: "Du kan finde flere plugins på GitHub: ",
},
Item: {
Info: (count: number) => `${count} metode`,
View: "Vis",
Edit: "Rediger",
Delete: "Slet",
DeleteConfirm: "Vil du slette?",
},
Auth: {
None: "Ingen",
Basic: "Basic",
Bearer: "Bearer",
Custom: "Tilpasset",
CustomHeader: "Parameternavn",
Token: "Token",
Proxy: "Brug Proxy",
ProxyDescription: "Løs CORS-problemer med Proxy",
Location: "Sted",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) =>
`Rediger Plugin ${readonly ? "(skrivebeskyttet)" : ""}`,
Download: "Download",
Auth: "Godkendelsestype",
Content: "OpenAPI Schema",
Load: "Hent fra URL",
Method: "Metode",
Error: "Fejl i OpenAPI Schema",
},
},
Mask: {
Name: "Persona",
Page: {
Title: "Prompts som personaer",
SubTitle: (count: number) => `${count} skabeloner`,
Search: "Søg skabeloner",
Create: "Opret ny",
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "Vis",
Edit: "Rediger",
Delete: "Slet",
DeleteConfirm: "Vil du slette?",
},
EditModal: {
Title: (readonly: boolean) =>
`Rediger skabelon ${readonly ? "(skrivebeskyttet)" : ""}`,
Download: "Download",
Clone: "Klon",
},
Config: {
Avatar: "Chat-avatar",
Name: "Chat-navn",
Sync: {
Title: "Brug globale indstillinger",
SubTitle: "Gældende for denne chat",
Confirm: "Erstat nuværende indstillinger med globale?",
},
HideContext: {
Title: "Skjul ekstra prompts",
SubTitle: "Vis dem ikke på chat-skærmen",
},
Artifacts: {
Title: "Brug Artefakter",
SubTitle: "Gør det muligt at vise HTML-sider",
},
CodeFold: {
Title: "Fold kode sammen",
SubTitle: "Luk/åbn lange kodestykker automatisk",
},
Share: {
Title: "Del denne persona",
SubTitle: "Få et link til denne skabelon",
Action: "Kopiér link",
},
},
},
NewChat: {
Return: "Tilbage",
Skip: "Start straks",
Title: "Vælg en persona",
SubTitle: "Chat med den persona, du vælger",
More: "Se flere",
NotShow: "Vis ikke igen",
ConfirmNoShow:
"Er du sikker på, at du ikke vil se det igen? Du kan altid slå det til under indstillinger.",
},
UI: {
Confirm: "OK",
Cancel: "Fortryd",
Close: "Luk",
Create: "Opret",
Edit: "Rediger",
Export: "Eksporter",
Import: "Importér",
Sync: "Synk",
Config: "Konfigurer",
},
Exporter: {
Description: {
Title: "Kun beskeder efter sidste rydning vises",
},
Model: "Model",
Messages: "Beskeder",
Topic: "Emne",
Time: "Tid",
},
URLCommand: {
Code: "Så ud til, at der var en kode i linket. Vil du bruge den?",
Settings: "Så ud til, at der var indstillinger i linket. Vil du bruge dem?",
},
SdPanel: {
Prompt: "Prompt",
NegativePrompt: "Negativ prompt",
PleaseInput: (name: string) => `Indtast: ${name}`,
AspectRatio: "Billedformat",
ImageStyle: "Stil",
OutFormat: "Uddataformat",
AIModel: "AI-model",
ModelVersion: "Version",
Submit: "Send",
ParamIsRequired: (name: string) => `${name} er krævet`,
Styles: {
D3Model: "3d-model",
AnalogFilm: "analog-film",
Anime: "anime",
Cinematic: "cinematisk",
ComicBook: "tegneserie",
DigitalArt: "digital-art",
Enhance: "enhance",
FantasyArt: "fantasy-art",
Isometric: "isometric",
LineArt: "line-art",
LowPoly: "low-poly",
ModelingCompound: "modeling-compound",
NeonPunk: "neon-punk",
Origami: "origami",
Photographic: "fotografisk",
PixelArt: "pixel-art",
TileTexture: "tile-texture",
},
},
Sd: {
SubTitle: (count: number) => `${count} billeder`,
Actions: {
Params: "Se indstillinger",
Copy: "Kopiér prompt",
Delete: "Slet",
Retry: "Prøv igen",
ReturnHome: "Til forsiden",
History: "Historik",
},
EmptyRecord: "Ingen billeder endnu",
Status: {
Name: "Status",
Success: "Ok",
Error: "Fejl",
Wait: "Venter",
Running: "I gang",
},
Danger: {
Delete: "Vil du slette?",
},
GenerateParams: "Genereringsvalg",
Detail: "Detaljer",
},
};
export default da;

View File

@@ -2,6 +2,7 @@ import cn from "./cn";
import en from "./en";
import pt from "./pt";
import tw from "./tw";
import da from "./da";
import id from "./id";
import fr from "./fr";
import es from "./es";
@@ -30,6 +31,7 @@ const ALL_LANGS = {
en,
tw,
pt,
da,
jp,
ko,
id,
@@ -56,6 +58,7 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
en: "English",
pt: "Português",
tw: "繁體中文",
da: "Dansk",
jp: "日本語",
ko: "한국어",
id: "Indonesia",
@@ -141,6 +144,7 @@ export const STT_LANG_MAP: Record<Lang, string> = {
en: "en-US",
pt: "pt-BR",
tw: "zh-TW",
da: "da-DK",
jp: "ja-JP",
ko: "ko-KR",
id: "id-ID",

View File

@@ -3,7 +3,7 @@ import {
UPLOAD_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { RequestMessage } from "@/app/client/api";
import { MultimodalContent, RequestMessage } from "@/app/client/api";
import Locale from "@/app/locales";
import {
EventStreamContentType,
@@ -70,8 +70,9 @@ export function compressImage(file: Blob, maxSize: number): Promise<string> {
});
}
export async function preProcessImageContent(
export async function preProcessImageContentBase(
content: RequestMessage["content"],
transformImageUrl: (url: string) => Promise<{ [key: string]: any }>,
) {
if (typeof content === "string") {
return content;
@@ -81,7 +82,7 @@ export async function preProcessImageContent(
if (part?.type == "image_url" && part?.image_url?.url) {
try {
const url = await cacheImageToBase64Image(part?.image_url?.url);
result.push({ type: part.type, image_url: { url } });
result.push(await transformImageUrl(url));
} catch (error) {
console.error("Error processing image URL:", error);
}
@@ -92,6 +93,23 @@ export async function preProcessImageContent(
return result;
}
export async function preProcessImageContent(
content: RequestMessage["content"],
) {
return preProcessImageContentBase(content, async (url) => ({
type: "image_url",
image_url: { url },
})) as Promise<MultimodalContent[] | string>;
}
export async function preProcessImageContentForAlibabaDashScope(
content: RequestMessage["content"],
) {
return preProcessImageContentBase(content, async (url) => ({
image: url,
}));
}
const imageCaches: Record<string, string> = {};
export function cacheImageToBase64Image(imageUrl: string) {
if (imageUrl.includes(CACHE_URL_PREFIX)) {

View File

@@ -15,6 +15,8 @@ const config: Config = {
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
},
extensionsToTreatAsEsm: [".ts", ".tsx"],
injectGlobals: true,
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async

View File

@@ -1,24 +1,22 @@
// Learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
import { jest } from "@jest/globals";
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({}),
json: () => Promise.resolve([]),
headers: new Headers(),
redirected: false,
statusText: "OK",
type: "basic",
url: "",
clone: function () {
return this;
},
body: null,
bodyUsed: false,
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
blob: () => Promise.resolve(new Blob()),
formData: () => Promise.resolve(new FormData()),
text: () => Promise.resolve(""),
}),
} as Response),
);

View File

@@ -17,8 +17,8 @@
"prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
"test": "jest --watch",
"test:ci": "jest --ci"
"test": "node --no-warnings --experimental-vm-modules $(yarn bin jest) --watch",
"test:ci": "node --no-warnings --experimental-vm-modules $(yarn bin jest) --ci"
},
"dependencies": {
"@fortaine/fetch-event-source": "^3.0.6",

View File

@@ -1,3 +1,4 @@
import { jest } from "@jest/globals";
import { isVisionModel } from "../app/utils";
describe("isVisionModel", () => {
@@ -50,7 +51,7 @@ describe("isVisionModel", () => {
test("should identify models from VISION_MODELS env var", () => {
process.env.VISION_MODELS = "custom-vision-model,another-vision-model";
expect(isVisionModel("custom-vision-model")).toBe(true);
expect(isVisionModel("another-vision-model")).toBe(true);
expect(isVisionModel("unrelated-model")).toBe(false);
@@ -64,4 +65,4 @@ describe("isVisionModel", () => {
expect(isVisionModel("unrelated-model")).toBe(false);
expect(isVisionModel("gpt-4-vision")).toBe(true);
});
});
});