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