diff --git a/frontend/README.md b/frontend/README.md index 77a890e0..a7ded3cb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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 | diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index e5d2b9e2..aefc3b61 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,3 +1,4 @@ +packages: [] ignoredBuiltDependencies: - esbuild - sharp diff --git a/frontend/src/app/mock/api/models/route.ts b/frontend/src/app/mock/api/models/route.ts index 5768e160..e0a2eb1e 100644 --- a/frontend/src/app/mock/api/models/route.ts +++ b/frontend/src/app/mock/api/models/route.ts @@ -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, }, diff --git a/frontend/src/app/mock/api/threads/search/route.ts b/frontend/src/app/mock/api/threads/search/route.ts index 93c48348..74ba79a3 100644 --- a/frontend/src/app/mock/api/threads/search/route.ts +++ b/frontend/src/app/mock/api/threads/search/route.ts @@ -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 & { + 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((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; + 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); } diff --git a/frontend/src/components/landing/sections/case-study-section.tsx b/frontend/src/components/landing/sections/case-study-section.tsx index 9a1fd51c..0ae2f667 100644 --- a/frontend/src/components/landing/sections/case-study-section.tsx +++ b/frontend/src/components/landing/sections/case-study-section.tsx @@ -55,7 +55,7 @@ export function CaseStudySection({ className }: { className?: string }) { {caseStudies.map((caseStudy) => ( diff --git a/frontend/src/components/ui/galaxy.jsx b/frontend/src/components/ui/galaxy.jsx index 549e0e8e..c1d20ab8 100644 --- a/frontend/src/components/ui/galaxy.jsx +++ b/frontend/src/components/ui/galaxy.jsx @@ -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); diff --git a/frontend/src/components/workspace/artifacts/index.ts b/frontend/src/components/workspace/artifacts/index.ts index bedf505e..0e589d5f 100644 --- a/frontend/src/components/workspace/artifacts/index.ts +++ b/frontend/src/components/workspace/artifacts/index.ts @@ -1,3 +1,4 @@ export * from "./artifact-file-detail"; export * from "./artifact-file-list"; +export * from "./artifact-trigger"; export * from "./context"; diff --git a/frontend/src/components/workspace/messages/context.ts b/frontend/src/components/workspace/messages/context.ts index 3d1ee827..967cf008 100644 --- a/frontend/src/components/workspace/messages/context.ts +++ b/frontend/src/components/workspace/messages/context.ts @@ -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; + thread: BaseStream; + isMock?: boolean; } export const ThreadContext = createContext( diff --git a/frontend/src/components/workspace/messages/markdown-content.tsx b/frontend/src/components/workspace/messages/markdown-content.tsx index 2e240d8e..4b61fc9f 100644 --- a/frontend/src/components/workspace/messages/markdown-content.tsx +++ b/frontend/src/components/workspace/messages/markdown-content.tsx @@ -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) => { + a: (props: AnchorHTMLAttributes) => { if (typeof props.children === "string") { const match = /^citation:(.+)$/.exec(props.children); if (match) { @@ -38,7 +43,16 @@ export function MarkdownContent({ return {text}; } } - return ; + const { className, target, rel, ...rest } = props; + const external = isExternalUrl(props.href); + return ( + + ); }, ...componentsFromProps, }; diff --git a/frontend/src/components/workspace/settings/appearance-settings-page.tsx b/frontend/src/components/workspace/settings/appearance-settings-page.tsx index 125fdaea..2e76d68e 100644 --- a/frontend/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend/src/components/workspace/settings/appearance-settings-page.tsx @@ -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() { >