feat(frontend): 合并与 github/main 的安全无冲突更新
This commit is contained in:
parent
63a5cc22c2
commit
0d255e9ac4
|
|
@ -41,6 +41,12 @@ pnpm dev
|
|||
# Type check
|
||||
pnpm typecheck
|
||||
|
||||
# Check formatting
|
||||
pnpm format
|
||||
|
||||
# Apply formatting
|
||||
pnpm format:write
|
||||
|
||||
# Lint
|
||||
pnpm lint
|
||||
|
||||
|
|
@ -101,8 +107,8 @@ src/
|
|||
│ └── utils/ # Utility functions
|
||||
├── hooks/ # Custom React hooks
|
||||
├── lib/ # Shared libraries & utilities
|
||||
├── server/ # Server-side code (Not available yet)
|
||||
│ └── better-auth/ # Authentication setup (Not available yet)
|
||||
├── server/ # Server-side code
|
||||
│ └── better-auth/ # Authentication setup and session helpers
|
||||
└── styles/ # Global styles
|
||||
```
|
||||
|
||||
|
|
@ -113,6 +119,8 @@ src/
|
|||
| `pnpm dev` | Start development server with Turbopack |
|
||||
| `pnpm build` | Build for production |
|
||||
| `pnpm start` | Start production server |
|
||||
| `pnpm format` | Check formatting with Prettier |
|
||||
| `pnpm format:write` | Apply formatting with Prettier |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm lint:fix` | Fix ESLint issues |
|
||||
| `pnpm typecheck` | Run TypeScript type checking |
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
packages: []
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
|
|
|
|||
|
|
@ -4,24 +4,28 @@ export function GET() {
|
|||
{
|
||||
id: "doubao-seed-1.8",
|
||||
name: "doubao-seed-1.8",
|
||||
model: "doubao-seed-1-8",
|
||||
display_name: "Doubao Seed 1.8",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "deepseek-v3.2",
|
||||
name: "deepseek-v3.2",
|
||||
model: "deepseek-chat",
|
||||
display_name: "DeepSeek v3.2",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5",
|
||||
name: "gpt-5",
|
||||
model: "gpt-5",
|
||||
display_name: "GPT-5",
|
||||
supports_thinking: true,
|
||||
},
|
||||
{
|
||||
id: "gemini-3-pro",
|
||||
name: "gemini-3-pro",
|
||||
model: "gemini-3-pro",
|
||||
display_name: "Gemini 3 Pro",
|
||||
supports_thinking: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,27 +1,84 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export function POST() {
|
||||
type ThreadSearchRequest = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: "updated_at" | "created_at";
|
||||
sortOrder?: "asc" | "desc";
|
||||
};
|
||||
|
||||
type MockThreadSearchResult = Record<string, unknown> & {
|
||||
thread_id: string;
|
||||
updated_at: string | undefined;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = ((await request.json().catch(() => ({}))) ?? {}) as ThreadSearchRequest;
|
||||
|
||||
const rawLimit = body.limit;
|
||||
let limit = 50;
|
||||
if (typeof rawLimit === "number") {
|
||||
const normalizedLimit = Math.max(0, Math.floor(rawLimit));
|
||||
if (!Number.isNaN(normalizedLimit)) {
|
||||
limit = normalizedLimit;
|
||||
}
|
||||
}
|
||||
|
||||
const rawOffset = body.offset;
|
||||
let offset = 0;
|
||||
if (typeof rawOffset === "number") {
|
||||
const normalizedOffset = Math.max(0, Math.floor(rawOffset));
|
||||
if (!Number.isNaN(normalizedOffset)) {
|
||||
offset = normalizedOffset;
|
||||
}
|
||||
}
|
||||
const sortBy = body.sortBy ?? "updated_at";
|
||||
const sortOrder = body.sortOrder ?? "desc";
|
||||
|
||||
const threadsDir = fs.readdirSync(
|
||||
path.resolve(process.cwd(), "public/demo/threads"),
|
||||
{
|
||||
withFileTypes: true,
|
||||
},
|
||||
);
|
||||
|
||||
const threadData = threadsDir
|
||||
.map((threadId) => {
|
||||
.map<MockThreadSearchResult | null>((threadId) => {
|
||||
if (threadId.isDirectory() && !threadId.name.startsWith(".")) {
|
||||
const threadData = fs.readFileSync(
|
||||
path.resolve(`public/demo/threads/${threadId.name}/thread.json`),
|
||||
"utf8",
|
||||
);
|
||||
const threadData = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(`public/demo/threads/${threadId.name}/thread.json`),
|
||||
"utf8",
|
||||
),
|
||||
) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
...threadData,
|
||||
thread_id: threadId.name,
|
||||
values: JSON.parse(threadData).values,
|
||||
updated_at:
|
||||
typeof threadData.updated_at === "string"
|
||||
? threadData.updated_at
|
||||
: typeof threadData.created_at === "string"
|
||||
? threadData.created_at
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return Response.json(threadData);
|
||||
.filter((thread): thread is MockThreadSearchResult => thread !== null)
|
||||
.sort((a, b) => {
|
||||
const aTimestamp = a[sortBy];
|
||||
const bTimestamp = b[sortBy];
|
||||
const aParsed =
|
||||
typeof aTimestamp === "string" ? Date.parse(aTimestamp) : 0;
|
||||
const bParsed =
|
||||
typeof bTimestamp === "string" ? Date.parse(bTimestamp) : 0;
|
||||
const aValue = Number.isNaN(aParsed) ? 0 : aParsed;
|
||||
const bValue = Number.isNaN(bParsed) ? 0 : bParsed;
|
||||
return sortOrder === "asc" ? aValue - bValue : bValue - aValue;
|
||||
});
|
||||
|
||||
const pagedThreads = threadData.slice(offset, offset + limit);
|
||||
return Response.json(pagedThreads);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function CaseStudySection({ className }: { className?: string }) {
|
|||
{caseStudies.map((caseStudy) => (
|
||||
<Link
|
||||
key={caseStudy.title}
|
||||
href={pathOfThread(caseStudy.threadId)}
|
||||
href={pathOfThread(caseStudy.threadId) + "?mock=true"}
|
||||
target="_blank"
|
||||
>
|
||||
<Card className="group/card relative h-64 overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -198,11 +198,28 @@ export default function Galaxy({
|
|||
useEffect(() => {
|
||||
if (!ctnDom.current) return;
|
||||
const ctn = ctnDom.current;
|
||||
const renderer = new Renderer({
|
||||
alpha: transparent,
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
|
||||
let renderer;
|
||||
try {
|
||||
renderer = new Renderer({
|
||||
alpha: transparent,
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Galaxy: WebGL is not available. The galaxy background will not be rendered.",
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const gl = renderer.gl;
|
||||
if (!gl) {
|
||||
console.warn(
|
||||
"Galaxy: WebGL context is null. The galaxy background will not be rendered.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (transparent) {
|
||||
gl.enable(gl.BLEND);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./artifact-file-detail";
|
||||
export * from "./artifact-file-list";
|
||||
export * from "./artifact-trigger";
|
||||
export * from "./context";
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
|
||||
export interface ThreadContextType {
|
||||
threadId: string;
|
||||
thread: UseStream<AgentThreadState>;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
isMock?: boolean;
|
||||
}
|
||||
|
||||
export const ThreadContext = createContext<ThreadContextType | undefined>(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { HTMLAttributes } from "react";
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
|
||||
import {
|
||||
MessageResponse,
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
|
||||
function isExternalUrl(href: string | undefined): boolean {
|
||||
return !!href && /^https?:\/\//.test(href);
|
||||
}
|
||||
|
||||
export type MarkdownContentProps = {
|
||||
content: string;
|
||||
isLoading: boolean;
|
||||
|
|
@ -30,7 +35,7 @@ export function MarkdownContent({
|
|||
}: MarkdownContentProps) {
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
a: (props: HTMLAttributes<HTMLAnchorElement>) => {
|
||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
if (typeof props.children === "string") {
|
||||
const match = /^citation:(.+)$/.exec(props.children);
|
||||
if (match) {
|
||||
|
|
@ -38,7 +43,16 @@ export function MarkdownContent({
|
|||
return <CitationLink {...props}>{text}</CitationLink>;
|
||||
}
|
||||
}
|
||||
return <a {...props} />;
|
||||
const { className, target, rel, ...rest } = props;
|
||||
const external = isExternalUrl(props.href);
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
className={cn("text-primary underline decoration-primary/30 underline-offset-2 hover:decoration-primary/60 transition-colors", className)}
|
||||
target={target ?? (external ? "_blank" : undefined)}
|
||||
rel={rel ?? (external ? "noopener noreferrer" : undefined)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
...componentsFromProps,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { enUS, zhCN, type Locale } from "@/core/i18n";
|
||||
import { enUS, isLocale, zhCN, type Locale } from "@/core/i18n";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -89,7 +89,11 @@ export function AppearanceSettingsPage() {
|
|||
>
|
||||
<Select
|
||||
value={locale}
|
||||
onValueChange={(value) => changeLocale(value as Locale)}
|
||||
onValueChange={(value) => {
|
||||
if (isLocale(value)) {
|
||||
changeLocale(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,50 @@
|
|||
import type { BaseStream } from "@langchain/langgraph-sdk";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
|
||||
import { useThreadChat } from "./chats";
|
||||
import { FlipDisplay } from "./flip-display";
|
||||
|
||||
export function ThreadTitle({
|
||||
threadTitle,
|
||||
threadId,
|
||||
thread,
|
||||
}: {
|
||||
className?: string;
|
||||
threadId: string;
|
||||
threadTitle: string;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
}) {
|
||||
return <FlipDisplay uniqueKey={threadTitle}>{threadTitle}</FlipDisplay>;
|
||||
const { t } = useI18n();
|
||||
const { isNewThread } = useThreadChat();
|
||||
useEffect(() => {
|
||||
let _title = t.pages.untitled;
|
||||
|
||||
if (thread.values?.title) {
|
||||
_title = thread.values.title;
|
||||
} else if (isNewThread) {
|
||||
_title = t.pages.newChat;
|
||||
}
|
||||
if (thread.isThreadLoading) {
|
||||
document.title = `Loading... - ${t.pages.appName}`;
|
||||
} else {
|
||||
document.title = `${_title} - ${t.pages.appName}`;
|
||||
}
|
||||
}, [
|
||||
isNewThread,
|
||||
t.pages.newChat,
|
||||
t.pages.untitled,
|
||||
t.pages.appName,
|
||||
thread.isThreadLoading,
|
||||
thread.values,
|
||||
]);
|
||||
|
||||
if (!thread.values?.title) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FlipDisplay uniqueKey={threadId}>
|
||||
{thread.values.title ?? "Untitled"}
|
||||
</FlipDisplay>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ChevronUpIcon, ListTodoIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Todo } from "@/core/todos";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -13,7 +14,7 @@ import {
|
|||
export function TodoList({
|
||||
className,
|
||||
todos,
|
||||
collapsed = false,
|
||||
collapsed: controlledCollapsed,
|
||||
hidden = false,
|
||||
onToggle,
|
||||
}: {
|
||||
|
|
@ -23,6 +24,18 @@ export function TodoList({
|
|||
hidden?: boolean;
|
||||
onToggle?: () => void;
|
||||
}) {
|
||||
const [internalCollapsed, setInternalCollapsed] = useState(true);
|
||||
const isControlled = controlledCollapsed !== undefined;
|
||||
const collapsed = isControlled ? controlledCollapsed : internalCollapsed;
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isControlled) {
|
||||
onToggle?.();
|
||||
} else {
|
||||
setInternalCollapsed((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -35,9 +48,7 @@ export function TodoList({
|
|||
className={cn(
|
||||
"bg-accent flex min-h-8 shrink-0 cursor-pointer items-center justify-between px-4 text-sm transition-all duration-300 ease-out",
|
||||
)}
|
||||
onClick={() => {
|
||||
onToggle?.();
|
||||
}}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="text-muted-foreground">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { MessagesSquare } from "lucide-react";
|
||||
import { BotIcon, MessagesSquare } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
|
|
@ -26,6 +26,17 @@ export function WorkspaceNavChatList() {
|
|||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={pathname.startsWith("/workspace/agents")}
|
||||
asChild
|
||||
>
|
||||
<Link className="text-muted-foreground" href="/workspace/agents">
|
||||
<BotIcon />
|
||||
<span>{t.sidebar.agents}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,41 @@ import { Client as LangGraphClient } from "@langchain/langgraph-sdk/client";
|
|||
|
||||
import { getLangGraphBaseURL } from "../config";
|
||||
|
||||
let _singleton: LangGraphClient | null = null;
|
||||
export function getAPIClient(): LangGraphClient {
|
||||
_singleton ??= new LangGraphClient({
|
||||
apiUrl: getLangGraphBaseURL(),
|
||||
import { sanitizeRunStreamOptions } from "./stream-mode";
|
||||
|
||||
function createCompatibleClient(isMock?: boolean): LangGraphClient {
|
||||
const client = new LangGraphClient({
|
||||
apiUrl: getLangGraphBaseURL(isMock),
|
||||
});
|
||||
return _singleton;
|
||||
|
||||
const originalRunStream = client.runs.stream.bind(client.runs);
|
||||
client.runs.stream = ((threadId, assistantId, payload) =>
|
||||
originalRunStream(
|
||||
threadId,
|
||||
assistantId,
|
||||
sanitizeRunStreamOptions(payload),
|
||||
)) as typeof client.runs.stream;
|
||||
|
||||
const originalJoinStream = client.runs.joinStream.bind(client.runs);
|
||||
client.runs.joinStream = ((threadId, runId, options) =>
|
||||
originalJoinStream(
|
||||
threadId,
|
||||
runId,
|
||||
sanitizeRunStreamOptions(options),
|
||||
)) as typeof client.runs.joinStream;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
const _clients = new Map<string, LangGraphClient>();
|
||||
export function getAPIClient(isMock?: boolean): LangGraphClient {
|
||||
const cacheKey = isMock ? "mock" : "default";
|
||||
let client = _clients.get(cacheKey);
|
||||
|
||||
if (!client) {
|
||||
client = createCompatibleClient(isMock);
|
||||
_clients.set(cacheKey, client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,17 +17,18 @@ export function useArtifactContent({
|
|||
const isWriteFile = useMemo(() => {
|
||||
return filepath.startsWith("write-file:");
|
||||
}, [filepath]);
|
||||
const { thread } = useThread();
|
||||
const { thread, isMock } = useThread();
|
||||
const content = useMemo(() => {
|
||||
if (isWriteFile) {
|
||||
return loadArtifactContentFromToolCall({ url: filepath, thread });
|
||||
}
|
||||
return null;
|
||||
}, [filepath, isWriteFile, thread]);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["artifact", filepath, threadId],
|
||||
queryKey: ["artifact", filepath, threadId, isMock],
|
||||
queryFn: () => {
|
||||
return loadArtifactContent({ filepath, threadId });
|
||||
return loadArtifactContent({ filepath, threadId, isMock });
|
||||
},
|
||||
enabled,
|
||||
// Cache artifact content for 5 minutes to avoid repeated fetches (especially for .skill ZIP extraction)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
import type { AgentThreadState } from "../threads";
|
||||
|
||||
|
|
@ -7,15 +7,17 @@ import { urlOfArtifact } from "./utils";
|
|||
export async function loadArtifactContent({
|
||||
filepath,
|
||||
threadId,
|
||||
isMock,
|
||||
}: {
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
isMock?: boolean;
|
||||
}) {
|
||||
let enhancedFilepath = filepath;
|
||||
if (filepath.endsWith(".skill")) {
|
||||
enhancedFilepath = filepath + "/SKILL.md";
|
||||
}
|
||||
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId });
|
||||
const url = urlOfArtifact({ filepath: enhancedFilepath, threadId, isMock });
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
return text;
|
||||
|
|
@ -26,7 +28,7 @@ export function loadArtifactContentFromToolCall({
|
|||
thread,
|
||||
}: {
|
||||
url: string;
|
||||
thread: UseStream<AgentThreadState>;
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
}) {
|
||||
const url = new URL(urlString);
|
||||
const toolCallId = url.searchParams.get("tool_call_id");
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@ export function urlOfArtifact({
|
|||
filepath,
|
||||
threadId,
|
||||
download = false,
|
||||
isMock = false,
|
||||
}: {
|
||||
filepath: string;
|
||||
threadId: string;
|
||||
download?: boolean;
|
||||
isMock?: boolean;
|
||||
}) {
|
||||
if (isMock) {
|
||||
return `${getBackendBaseURL()}/mock/api/threads/${threadId}/artifacts${filepath}${download ? "?download=true" : ""}`;
|
||||
}
|
||||
return `${getBackendBaseURL()}/api/threads/${threadId}/artifacts${filepath}${download ? "?download=true" : ""}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,35 @@
|
|||
import { env } from "@/env";
|
||||
|
||||
function getBaseOrigin() {
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.origin;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getBackendBaseURL() {
|
||||
if (env.NEXT_PUBLIC_BACKEND_BASE_URL) {
|
||||
return env.NEXT_PUBLIC_BACKEND_BASE_URL;
|
||||
return new URL(
|
||||
env.NEXT_PUBLIC_BACKEND_BASE_URL,
|
||||
getBaseOrigin(),
|
||||
).toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function getLangGraphBaseURL() {
|
||||
export function getLangGraphBaseURL(isMock?: boolean) {
|
||||
if (env.NEXT_PUBLIC_LANGGRAPH_BASE_URL) {
|
||||
return env.NEXT_PUBLIC_LANGGRAPH_BASE_URL;
|
||||
return new URL(
|
||||
env.NEXT_PUBLIC_LANGGRAPH_BASE_URL,
|
||||
getBaseOrigin(),
|
||||
).toString();
|
||||
} else if (isMock) {
|
||||
if (typeof window !== "undefined") {
|
||||
return `${window.location.origin}/mock/api`;
|
||||
}
|
||||
return "http://localhost:3000/mock/api";
|
||||
} else {
|
||||
// LangGraph SDK requires a full URL, construct it from current origin
|
||||
if (typeof window !== "undefined") {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ import { getLocaleFromCookie, setLocaleInCookie } from "./cookies";
|
|||
import { enUS } from "./locales/en-US";
|
||||
import { zhCN } from "./locales/zh-CN";
|
||||
|
||||
import { detectLocale, type Locale, type Translations } from "./index";
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
detectLocale,
|
||||
normalizeLocale,
|
||||
type Locale,
|
||||
type Translations,
|
||||
} from "./index";
|
||||
|
||||
const translations: Record<Locale, Translations> = {
|
||||
"en-US": enUS,
|
||||
|
|
@ -17,7 +23,7 @@ const translations: Record<Locale, Translations> = {
|
|||
export function useI18n() {
|
||||
const { locale, setLocale } = useI18nContext();
|
||||
|
||||
const t = translations[locale];
|
||||
const t = translations[locale] ?? translations[DEFAULT_LOCALE];
|
||||
|
||||
const changeLocale = (newLocale: Locale) => {
|
||||
setLocale(newLocale);
|
||||
|
|
@ -26,12 +32,19 @@ export function useI18n() {
|
|||
|
||||
// Initialize locale on mount
|
||||
useEffect(() => {
|
||||
const saved = getLocaleFromCookie() as Locale | null;
|
||||
if (!saved) {
|
||||
const detected = detectLocale();
|
||||
setLocale(detected);
|
||||
setLocaleInCookie(detected);
|
||||
const saved = getLocaleFromCookie();
|
||||
if (saved) {
|
||||
const normalizedSaved = normalizeLocale(saved);
|
||||
setLocale(normalizedSaved);
|
||||
if (saved !== normalizedSaved) {
|
||||
setLocaleInCookie(normalizedSaved);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const detected = detectLocale();
|
||||
setLocale(detected);
|
||||
setLocaleInCookie(detected);
|
||||
}, [setLocale]);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
export { enUS } from "./locales/en-US";
|
||||
export { zhCN } from "./locales/zh-CN";
|
||||
export type { Translations } from "./locales/types";
|
||||
|
||||
export type Locale = "en-US" | "zh-CN";
|
||||
|
||||
// Helper function to detect browser locale
|
||||
export function detectLocale(): Locale {
|
||||
if (typeof window === "undefined") {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
const browserLang =
|
||||
navigator.language ||
|
||||
(navigator as unknown as { userLanguage: string }).userLanguage;
|
||||
|
||||
// Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.)
|
||||
if (browserLang.toLowerCase().startsWith("zh")) {
|
||||
return "zh-CN";
|
||||
}
|
||||
|
||||
return "en-US";
|
||||
}
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
SUPPORTED_LOCALES,
|
||||
detectLocale,
|
||||
isLocale,
|
||||
normalizeLocale,
|
||||
} from "./locale";
|
||||
export type { Locale } from "./locale";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { cookies } from "next/headers";
|
||||
|
||||
export type Locale = "en-US" | "zh-CN";
|
||||
import { normalizeLocale, type Locale } from "./locale";
|
||||
|
||||
export async function detectLocaleServer(): Promise<Locale> {
|
||||
const cookieStore = await cookies();
|
||||
const locale = cookieStore.get("locale")?.value ?? "en-US";
|
||||
return locale as Locale;
|
||||
let locale = cookieStore.get("locale")?.value;
|
||||
if (locale !== undefined) {
|
||||
try {
|
||||
locale = decodeURIComponent(locale);
|
||||
} catch {
|
||||
// Keep raw cookie value when decoding fails.
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeLocale(locale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { getBackendBaseURL } from "../config";
|
|||
import type { Model } from "./types";
|
||||
|
||||
export async function loadModels() {
|
||||
const res = fetch(`${getBackendBaseURL()}/api/models`);
|
||||
const { models } = (await (await res).json()) as { models: Model[] };
|
||||
const res = await fetch(`${getBackendBaseURL()}/api/models`);
|
||||
const { models } = (await res.json()) as { models: Model[] };
|
||||
return models;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export function useModels({ enabled = true }: { enabled?: boolean } = {}) {
|
|||
queryKey: ["models"],
|
||||
queryFn: () => loadModels(),
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
return { models: data ?? [], isLoading, error };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
export interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
display_name: string;
|
||||
description?: string | null;
|
||||
supports_thinking?: boolean;
|
||||
supports_reasoning_effort?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const DEFAULT_LOCAL_SETTINGS: LocalSettings = {
|
|||
context: {
|
||||
model_name: undefined,
|
||||
mode: undefined,
|
||||
reasoning_effort: undefined,
|
||||
},
|
||||
layout: {
|
||||
sidebar_collapsed: false,
|
||||
|
|
@ -24,6 +25,7 @@ export interface LocalSettings {
|
|||
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
||||
> & {
|
||||
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||
};
|
||||
layout: {
|
||||
sidebar_collapsed: boolean;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { type BaseMessage } from "@langchain/core/messages";
|
||||
import type { Thread } from "@langchain/langgraph-sdk";
|
||||
import type { Message, Thread } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { Todo } from "../todos";
|
||||
|
||||
export interface AgentThreadState extends Record<string, unknown> {
|
||||
title: string;
|
||||
messages: BaseMessage[];
|
||||
messages: Message[];
|
||||
artifacts: string[];
|
||||
todos?: Todo[];
|
||||
}
|
||||
|
|
@ -18,4 +17,6 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
|||
thinking_enabled: boolean;
|
||||
is_plan_mode: boolean;
|
||||
subagent_enabled: boolean;
|
||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||
agent_name?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseMessage } from "@langchain/core/messages";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { AgentThread } from "./types";
|
||||
|
||||
|
|
@ -6,19 +6,19 @@ export function pathOfThread(threadId: string) {
|
|||
return `/workspace/chats/${threadId}`;
|
||||
}
|
||||
|
||||
export function textOfMessage(message: BaseMessage) {
|
||||
export function textOfMessage(message: Message) {
|
||||
if (typeof message.content === "string") {
|
||||
return message.content;
|
||||
} else if (Array.isArray(message.content)) {
|
||||
return message.content.find((part) => part.type === "text" && part.text)
|
||||
?.text as string;
|
||||
for (const part of message.content) {
|
||||
if (part.type === "text") {
|
||||
return part.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function titleOfThread(thread: AgentThread) {
|
||||
if (thread.values && "title" in thread.values) {
|
||||
return thread.values.title;
|
||||
}
|
||||
return "Untitled";
|
||||
return thread.values?.title ?? "Untitled";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue