revert(frontend): 回滚错误的 frontend 同步提交
This commit is contained in:
parent
42ad3f8336
commit
09c6a74ea3
|
|
@ -21,6 +21,8 @@ next-env.d.ts
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
docs
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,67 @@
|
||||||
{
|
{
|
||||||
"window.title": "${activeEditorShort}${separator}${separator}deer-flow/frontend"
|
"window.title": "${activeEditorShort}${separator}${separator}deer-flow/frontend",
|
||||||
|
"todo-tree.regex.regex": "((%|#|//|<!--|\\{/\\*|^\\s*\\*)\\s*($TAGS)|^\\s*- \\[ \\])",
|
||||||
|
"todo-tree.general.tags": [
|
||||||
|
"TODO:",
|
||||||
|
"BUG:",
|
||||||
|
"TAG:",
|
||||||
|
"DONE:",
|
||||||
|
"MARK:",
|
||||||
|
"TEST:",
|
||||||
|
"XXX:"
|
||||||
|
],
|
||||||
|
"todo-tree.regex.regexCaseSensitive": false,
|
||||||
|
"todo-tree.highlights.defaultHighlight": {
|
||||||
|
"foreground": "#000000",
|
||||||
|
"background": "#fff700",
|
||||||
|
"icon": "check",
|
||||||
|
"rulerColour": "#fff700",
|
||||||
|
"type": "tag",
|
||||||
|
"iconColour": "#fff700"
|
||||||
|
},
|
||||||
|
"todo-tree.highlights.customHighlight": {
|
||||||
|
"TODO:": {
|
||||||
|
"icon": "todo",
|
||||||
|
"background": "#fff700",
|
||||||
|
"rulerColour": "#fff700",
|
||||||
|
"iconColour": "#fff700"
|
||||||
|
},
|
||||||
|
"BUG:": {
|
||||||
|
"background": "#eb5c5c",
|
||||||
|
"icon": "bug",
|
||||||
|
"rulerColour": "#eb5c5c",
|
||||||
|
"iconColour": "#eb5c5c"
|
||||||
|
},
|
||||||
|
"TAG:": {
|
||||||
|
"background": "#38b2f4",
|
||||||
|
"icon": "tag",
|
||||||
|
"rulerColour": "#38b2f4",
|
||||||
|
"iconColour": "#38b2f4",
|
||||||
|
"rulerLane": "full"
|
||||||
|
},
|
||||||
|
"DONE:": {
|
||||||
|
"background": "#5eec95",
|
||||||
|
"icon": "check",
|
||||||
|
"rulerColour": "#5eec95",
|
||||||
|
"iconColour": "#5eec95"
|
||||||
|
},
|
||||||
|
"MARK:": {
|
||||||
|
"background": "#f90",
|
||||||
|
"icon": "note",
|
||||||
|
"rulerColour": "#f90",
|
||||||
|
"iconColour": "#f90"
|
||||||
|
},
|
||||||
|
"TEST:": {
|
||||||
|
"background": "#df7be6",
|
||||||
|
"icon": "flame",
|
||||||
|
"rulerColour": "#df7be6",
|
||||||
|
"iconColour": "#df7be6"
|
||||||
|
},
|
||||||
|
"XXX:": {
|
||||||
|
"background": "#d65d8e",
|
||||||
|
"icon": "versions",
|
||||||
|
"rulerColour": "#d65d8e",
|
||||||
|
"iconColour": "#d65d8e"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,12 +76,6 @@ src/
|
||||||
- **MagicUI** - Magic UI components
|
- **MagicUI** - Magic UI components
|
||||||
- **React Bits** - React bits components
|
- **React Bits** - React bits components
|
||||||
|
|
||||||
### Interaction Ownership
|
|
||||||
|
|
||||||
- `src/app/workspace/chats/[thread_id]/page.tsx` owns composer busy-state wiring.
|
|
||||||
- `src/core/threads/hooks.ts` owns pre-submit upload state and thread submission.
|
|
||||||
- `src/hooks/usePoseStream.ts` is a passive store selector; global WebSocket lifecycle stays in `App.tsx`.
|
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,22 @@
|
||||||
# Frontend Dockerfile
|
# Frontend Development Dockerfile
|
||||||
# Supports two targets:
|
FROM node:22-alpine
|
||||||
# --target dev — install deps only, run `pnpm dev` at container start
|
|
||||||
# --target prod — full build baked in, run `pnpm start` at container start (default if no --target is specified)
|
|
||||||
|
|
||||||
|
# Accept build argument for pnpm store path
|
||||||
ARG PNPM_STORE_PATH=/root/.local/share/pnpm/store
|
ARG PNPM_STORE_PATH=/root/.local/share/pnpm/store
|
||||||
|
|
||||||
# ── Base: shared setup ────────────────────────────────────────────────────────
|
# Install pnpm at specific version (matching package.json)
|
||||||
FROM node:22-alpine AS base
|
RUN corepack enable && corepack install -g pnpm@10.26.2
|
||||||
ARG PNPM_STORE_PATH
|
|
||||||
ARG NPM_REGISTRY
|
|
||||||
# Configure corepack registry before installing pnpm so the download itself
|
|
||||||
# succeeds in restricted networks (COREPACK_NPM_REGISTRY controls where
|
|
||||||
# corepack fetches package managers from).
|
|
||||||
RUN if [ -n "${NPM_REGISTRY}" ]; then \
|
|
||||||
export COREPACK_NPM_REGISTRY="${NPM_REGISTRY}"; \
|
|
||||||
fi && \
|
|
||||||
corepack enable && corepack install -g pnpm@10.26.2
|
|
||||||
RUN pnpm config set store-dir ${PNPM_STORE_PATH}
|
RUN pnpm config set store-dir ${PNPM_STORE_PATH}
|
||||||
# Optionally override npm registry for restricted networks (e.g. NPM_REGISTRY=https://registry.npmmirror.com)
|
|
||||||
RUN if [ -n "${NPM_REGISTRY}" ]; then pnpm config set registry "${NPM_REGISTRY}"; fi
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy frontend source code
|
||||||
COPY frontend ./frontend
|
COPY frontend ./frontend
|
||||||
|
|
||||||
# ── Dev: install only, CMD is overridden by docker-compose ───────────────────
|
# Install dependencies
|
||||||
FROM base AS dev
|
RUN sh -c "cd /app/frontend && pnpm install --frozen-lockfile"
|
||||||
RUN cd /app/frontend && pnpm install --frozen-lockfile
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# ── Builder: install + compile Next.js ───────────────────────────────────────
|
# Expose Next.js dev server port
|
||||||
FROM base AS builder
|
|
||||||
RUN cd /app/frontend && pnpm install --frozen-lockfile
|
|
||||||
# Skip env validation — runtime vars are injected by nginx/container
|
|
||||||
RUN cd /app/frontend && SKIP_ENV_VALIDATION=1 pnpm build
|
|
||||||
|
|
||||||
# ── Prod: minimal runtime with pre-built output ───────────────────────────────
|
|
||||||
FROM node:22-alpine AS prod
|
|
||||||
ARG PNPM_STORE_PATH
|
|
||||||
ARG NPM_REGISTRY
|
|
||||||
RUN if [ -n "${NPM_REGISTRY}" ]; then \
|
|
||||||
export COREPACK_NPM_REGISTRY="${NPM_REGISTRY}"; \
|
|
||||||
fi && \
|
|
||||||
corepack enable && corepack install -g pnpm@10.26.2
|
|
||||||
RUN pnpm config set store-dir ${PNPM_STORE_PATH}
|
|
||||||
RUN if [ -n "${NPM_REGISTRY}" ]; then pnpm config set registry "${NPM_REGISTRY}"; fi
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /app/frontend ./frontend
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["sh", "-c", "cd /app/frontend && pnpm start"]
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# --------------- 构建阶段 ---------------
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ARG PNPM_STORE_PATH=/root/.local/share/pnpm/store
|
||||||
|
ENV BETTER_AUTH_SECRET=any-random-string-123456
|
||||||
|
|
||||||
|
RUN corepack enable && corepack install -g pnpm@10.26.2
|
||||||
|
RUN pnpm config set store-dir ${PNPM_STORE_PATH}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY frontend/ .
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["pnpm", "start"]
|
||||||
|
|
@ -41,12 +41,6 @@ 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
|
||||||
|
|
||||||
|
|
@ -107,8 +101,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
|
├── server/ # Server-side code (Not available yet)
|
||||||
│ └── better-auth/ # Authentication setup and session helpers
|
│ └── better-auth/ # Authentication setup (Not available yet)
|
||||||
└── styles/ # Global styles
|
└── styles/ # Global styles
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -119,8 +113,6 @@ 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 |
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"demo:save": "node scripts/save-demo.js",
|
"demo:save": "node scripts/save-demo.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"check": "eslint . --ext .ts,.tsx && tsc --noEmit",
|
"check": "next lint && tsc --noEmit",
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||||
"format:write": "prettier --write .",
|
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||||
"lint": "eslint . --ext .ts,.tsx",
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
|
|
@ -59,15 +59,18 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"docx": "^9.6.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"hast": "^1.0.0",
|
"hast": "^1.0.0",
|
||||||
|
"html2pdf.js": "^0.14.0",
|
||||||
"katex": "^0.16.28",
|
"katex": "^0.16.28",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"marked": "^17.0.5",
|
||||||
"motion": "^12.26.2",
|
"motion": "^12.26.2",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"next": "^16.1.7",
|
"next": "^16.1.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nuxt-og-image": "^5.1.13",
|
"nuxt-og-image": "^5.1.13",
|
||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,3 @@
|
||||||
packages: []
|
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- esbuild
|
- esbuild
|
||||||
- sharp
|
- sharp
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import "@/styles/globals.css";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
import { type Metadata } from "next";
|
import { type Metadata } from "next";
|
||||||
|
import { Geist } from "next/font/google";
|
||||||
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { I18nProvider } from "@/core/i18n/context";
|
import { I18nProvider } from "@/core/i18n/context";
|
||||||
|
|
@ -12,12 +13,22 @@ export const metadata: Metadata = {
|
||||||
description: "A LangChain-based framework for building super agents.",
|
description: "A LangChain-based framework for building super agents.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const geist = Geist({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
});
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
const locale = await detectLocaleServer();
|
const locale = await detectLocaleServer();
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressContentEditableWarning suppressHydrationWarning>
|
<html
|
||||||
|
lang={locale}
|
||||||
|
className={geist.variable + ""}
|
||||||
|
suppressContentEditableWarning
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
<body>
|
<body>
|
||||||
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
|
<ThemeProvider attribute="class" enableSystem disableTransitionOnChange>
|
||||||
<I18nProvider initialLocale={locale}>{children}</I18nProvider>
|
<I18nProvider initialLocale={locale}>{children}</I18nProvider>
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,24 @@ 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,84 +1,27 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
type ThreadSearchRequest = {
|
export function POST() {
|
||||||
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<MockThreadSearchResult | null>((threadId) => {
|
.map((threadId) => {
|
||||||
if (threadId.isDirectory() && !threadId.name.startsWith(".")) {
|
if (threadId.isDirectory() && !threadId.name.startsWith(".")) {
|
||||||
const threadData = JSON.parse(
|
const threadData = fs.readFileSync(
|
||||||
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,
|
||||||
updated_at:
|
values: JSON.parse(threadData).values,
|
||||||
typeof threadData.updated_at === "string"
|
|
||||||
? threadData.updated_at
|
|
||||||
: typeof threadData.created_at === "string"
|
|
||||||
? threadData.created_at
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return false;
|
||||||
})
|
})
|
||||||
.filter((thread): thread is MockThreadSearchResult => thread !== null)
|
.filter(Boolean);
|
||||||
.sort((a, b) => {
|
return Response.json(threadData);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { PromptInputProvider } from "@/components/ai-elements/prompt-input";
|
|
||||||
import { ArtifactsProvider } from "@/components/workspace/artifacts";
|
|
||||||
import { SubtasksProvider } from "@/core/tasks/context";
|
|
||||||
|
|
||||||
export default function AgentChatLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SubtasksProvider>
|
|
||||||
<ArtifactsProvider>
|
|
||||||
<PromptInputProvider>{children}</PromptInputProvider>
|
|
||||||
</ArtifactsProvider>
|
|
||||||
</SubtasksProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BotIcon, PlusSquare } from "lucide-react";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { AgentWelcome } from "@/components/workspace/agent-welcome";
|
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
|
||||||
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
|
||||||
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
|
||||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
|
||||||
import { Tooltip } from "@/components/workspace/tooltip";
|
|
||||||
import { useAgent } from "@/core/agents";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
|
||||||
import { useLocalSettings } from "@/core/settings";
|
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
|
||||||
import { env } from "@/env";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export default function AgentChatPage() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const [settings, setSettings] = useLocalSettings();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { agent_name } = useParams<{
|
|
||||||
agent_name: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { agent } = useAgent(agent_name);
|
|
||||||
|
|
||||||
const { threadId, isNewThread, setIsNewThread } = useThreadChat();
|
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
|
||||||
const [thread, sendMessage] = useThreadStream({
|
|
||||||
threadId: isNewThread ? undefined : threadId,
|
|
||||||
context: { ...settings.context, agent_name: agent_name },
|
|
||||||
onStart: () => {
|
|
||||||
setIsNewThread(false);
|
|
||||||
// ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead.
|
|
||||||
history.replaceState(
|
|
||||||
null,
|
|
||||||
"",
|
|
||||||
`/workspace/agents/${agent_name}/chats/${threadId}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onFinish: (state) => {
|
|
||||||
if (document.hidden || !document.hasFocus()) {
|
|
||||||
let body = "Conversation finished";
|
|
||||||
const lastMessage = state.messages[state.messages.length - 1];
|
|
||||||
if (lastMessage) {
|
|
||||||
const textContent = textOfMessage(lastMessage);
|
|
||||||
if (textContent) {
|
|
||||||
body =
|
|
||||||
textContent.length > 200
|
|
||||||
? textContent.substring(0, 200) + "..."
|
|
||||||
: textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showNotification(state.title, { body });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
|
||||||
(message: PromptInputMessage) => {
|
|
||||||
void sendMessage(threadId, message, { agent_name });
|
|
||||||
},
|
|
||||||
[sendMessage, threadId, agent_name],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleStop = useCallback(async () => {
|
|
||||||
await thread.stop();
|
|
||||||
}, [thread]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
|
||||||
<ChatBox threadId={threadId}>
|
|
||||||
<div className="relative flex size-full min-h-0 justify-between">
|
|
||||||
<header
|
|
||||||
className={cn(
|
|
||||||
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center gap-2 px-4",
|
|
||||||
isNewThread
|
|
||||||
? "bg-background/0 backdrop-blur-none"
|
|
||||||
: "bg-background/80 shadow-xs backdrop-blur",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Agent badge */}
|
|
||||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
|
||||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
|
||||||
<span className="text-xs font-medium">
|
|
||||||
{agent?.name ?? agent_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center text-sm font-medium">
|
|
||||||
<ThreadTitle threadId={threadId} thread={thread} />
|
|
||||||
</div>
|
|
||||||
<div className="mr-4 flex items-center">
|
|
||||||
<Tooltip content={t.agents.newChat}>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/workspace/agents/${agent_name}/chats/new`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusSquare /> {t.agents.newChat}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<TokenUsageIndicator messages={thread.messages} />
|
|
||||||
<ExportTrigger threadId={threadId} />
|
|
||||||
<ArtifactTrigger />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="flex min-h-0 max-w-full grow flex-col">
|
|
||||||
<div className="flex size-full justify-center">
|
|
||||||
<MessageList
|
|
||||||
className={cn("size-full", !isNewThread && "pt-10")}
|
|
||||||
threadId={threadId}
|
|
||||||
thread={thread}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"relative w-full",
|
|
||||||
isNewThread && "-translate-y-[calc(50vh-96px)]",
|
|
||||||
isNewThread
|
|
||||||
? "max-w-(--container-width-sm)"
|
|
||||||
: "max-w-(--container-width-md)",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute -top-4 right-0 left-0 z-0">
|
|
||||||
<div className="absolute right-0 bottom-0 left-0">
|
|
||||||
<TodoList
|
|
||||||
className="bg-background/5"
|
|
||||||
todos={thread.values.todos ?? []}
|
|
||||||
hidden={
|
|
||||||
!thread.values.todos || thread.values.todos.length === 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<InputBox
|
|
||||||
className={cn("bg-background/5 w-full -translate-y-4")}
|
|
||||||
isNewThread={isNewThread}
|
|
||||||
threadId={threadId}
|
|
||||||
autoFocus={isNewThread}
|
|
||||||
status={
|
|
||||||
thread.error
|
|
||||||
? "error"
|
|
||||||
: thread.isLoading
|
|
||||||
? "streaming"
|
|
||||||
: "ready"
|
|
||||||
}
|
|
||||||
context={settings.context}
|
|
||||||
extraHeader={
|
|
||||||
isNewThread && (
|
|
||||||
<AgentWelcome agent={agent} agentName={agent_name} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onStop={handleStop}
|
|
||||||
/>
|
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
|
||||||
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
|
||||||
{t.common.notAvailableInDemoMode}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ChatBox>
|
|
||||||
</ThreadContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,252 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowLeftIcon, BotIcon, CheckCircleIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
PromptInput,
|
|
||||||
PromptInputFooter,
|
|
||||||
PromptInputSubmit,
|
|
||||||
PromptInputTextarea,
|
|
||||||
} from "@/components/ai-elements/prompt-input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { ArtifactsProvider } from "@/components/workspace/artifacts";
|
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
|
||||||
import type { Agent } from "@/core/agents";
|
|
||||||
import { checkAgentName, getAgent } from "@/core/agents/api";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
|
||||||
import { uuid } from "@/core/utils/uuid";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
type Step = "name" | "chat";
|
|
||||||
|
|
||||||
const NAME_RE = /^[A-Za-z0-9-]+$/;
|
|
||||||
|
|
||||||
export default function NewAgentPage() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// ── Step 1: name form ──────────────────────────────────────────────────────
|
|
||||||
const [step, setStep] = useState<Step>("name");
|
|
||||||
const [nameInput, setNameInput] = useState("");
|
|
||||||
const [nameError, setNameError] = useState("");
|
|
||||||
const [isCheckingName, setIsCheckingName] = useState(false);
|
|
||||||
const [agentName, setAgentName] = useState("");
|
|
||||||
const [agent, setAgent] = useState<Agent | null>(null);
|
|
||||||
// ── Step 2: chat ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Stable thread ID — all turns belong to the same thread
|
|
||||||
const threadId = useMemo(() => uuid(), []);
|
|
||||||
|
|
||||||
const [thread, sendMessage] = useThreadStream({
|
|
||||||
threadId: step === "chat" ? threadId : undefined,
|
|
||||||
context: {
|
|
||||||
mode: "flash",
|
|
||||||
is_bootstrap: true,
|
|
||||||
},
|
|
||||||
onToolEnd({ name }) {
|
|
||||||
if (name !== "setup_agent" || !agentName) return;
|
|
||||||
getAgent(agentName)
|
|
||||||
.then((fetched) => setAgent(fetched))
|
|
||||||
.catch(() => {
|
|
||||||
// agent write may not be flushed yet — ignore silently
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Handlers ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const handleConfirmName = useCallback(async () => {
|
|
||||||
const trimmed = nameInput.trim();
|
|
||||||
if (!trimmed) return;
|
|
||||||
if (!NAME_RE.test(trimmed)) {
|
|
||||||
setNameError(t.agents.nameStepInvalidError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setNameError("");
|
|
||||||
setIsCheckingName(true);
|
|
||||||
try {
|
|
||||||
const result = await checkAgentName(trimmed);
|
|
||||||
if (!result.available) {
|
|
||||||
setNameError(t.agents.nameStepAlreadyExistsError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setNameError(t.agents.nameStepCheckError);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
setIsCheckingName(false);
|
|
||||||
}
|
|
||||||
setAgentName(trimmed);
|
|
||||||
setStep("chat");
|
|
||||||
await sendMessage(threadId, {
|
|
||||||
text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed),
|
|
||||||
files: [],
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
nameInput,
|
|
||||||
sendMessage,
|
|
||||||
threadId,
|
|
||||||
t.agents.nameStepBootstrapMessage,
|
|
||||||
t.agents.nameStepInvalidError,
|
|
||||||
t.agents.nameStepAlreadyExistsError,
|
|
||||||
t.agents.nameStepCheckError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleNameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
void handleConfirmName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChatSubmit = useCallback(
|
|
||||||
async (text: string) => {
|
|
||||||
const trimmed = text.trim();
|
|
||||||
if (!trimmed || thread.isLoading) return;
|
|
||||||
await sendMessage(
|
|
||||||
threadId,
|
|
||||||
{ text: trimmed, files: [] },
|
|
||||||
{ agent_name: agentName },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[thread.isLoading, sendMessage, threadId, agentName],
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Shared header ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const header = (
|
|
||||||
<header className="flex shrink-0 items-center gap-3 border-b px-4 py-3">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={() => router.push("/workspace/agents")}
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-sm font-semibold">{t.agents.createPageTitle}</h1>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Step 1: name form ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if (step === "name") {
|
|
||||||
return (
|
|
||||||
<div className="flex size-full flex-col">
|
|
||||||
{header}
|
|
||||||
<main className="flex flex-1 flex-col items-center justify-center px-4">
|
|
||||||
<div className="w-full max-w-sm space-y-8">
|
|
||||||
<div className="space-y-3 text-center">
|
|
||||||
<div className="bg-primary/10 mx-auto flex h-14 w-14 items-center justify-center rounded-full">
|
|
||||||
<BotIcon className="text-primary h-7 w-7" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
{t.agents.nameStepTitle}
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
{t.agents.nameStepHint}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
placeholder={t.agents.nameStepPlaceholder}
|
|
||||||
value={nameInput}
|
|
||||||
onChange={(e) => {
|
|
||||||
setNameInput(e.target.value);
|
|
||||||
setNameError("");
|
|
||||||
}}
|
|
||||||
onKeyDown={handleNameKeyDown}
|
|
||||||
className={cn(nameError && "border-destructive")}
|
|
||||||
/>
|
|
||||||
{nameError && (
|
|
||||||
<p className="text-destructive text-sm">{nameError}</p>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => void handleConfirmName()}
|
|
||||||
disabled={!nameInput.trim() || isCheckingName}
|
|
||||||
>
|
|
||||||
{t.agents.nameStepContinue}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Step 2: chat ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
|
||||||
<ArtifactsProvider>
|
|
||||||
<div className="flex size-full flex-col">
|
|
||||||
{header}
|
|
||||||
|
|
||||||
<main className="flex min-h-0 flex-1 flex-col">
|
|
||||||
{/* ── Message area ── */}
|
|
||||||
<div className="flex min-h-0 flex-1 justify-center">
|
|
||||||
<MessageList
|
|
||||||
className="size-full pt-10"
|
|
||||||
threadId={threadId}
|
|
||||||
thread={thread}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Bottom action area ── */}
|
|
||||||
<div className="bg-background flex shrink-0 justify-center border-t px-4 py-4">
|
|
||||||
<div className="w-full max-w-(--container-width-md)">
|
|
||||||
{agent ? (
|
|
||||||
// ✅ Success card
|
|
||||||
<div className="flex flex-col items-center gap-4 rounded-2xl border py-8 text-center">
|
|
||||||
<CheckCircleIcon className="text-primary h-10 w-10" />
|
|
||||||
<p className="font-semibold">{t.agents.agentCreated}</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
router.push(
|
|
||||||
`/workspace/agents/${agentName}/chats/new`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t.agents.startChatting}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push("/workspace/agents")}
|
|
||||||
>
|
|
||||||
{t.agents.backToGallery}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// 📝 Normal input
|
|
||||||
<PromptInput
|
|
||||||
onSubmit={({ text }) => void handleChatSubmit(text)}
|
|
||||||
>
|
|
||||||
<PromptInputTextarea
|
|
||||||
autoFocus
|
|
||||||
placeholder={t.agents.createPageSubtitle}
|
|
||||||
disabled={thread.isLoading}
|
|
||||||
/>
|
|
||||||
<PromptInputFooter className="justify-end">
|
|
||||||
<PromptInputSubmit disabled={thread.isLoading} />
|
|
||||||
</PromptInputFooter>
|
|
||||||
</PromptInput>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ArtifactsProvider>
|
|
||||||
</ThreadContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { AgentGallery } from "@/components/workspace/agents/agent-gallery";
|
|
||||||
|
|
||||||
export default function AgentsPage() {
|
|
||||||
return <AgentGallery />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +1,482 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
import type { UseStream } from "@langchain/langgraph-sdk/react";
|
||||||
|
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
import { usePromptInputController } from "@/components/ai-elements/prompt-input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ChatBox,
|
DevDialog,
|
||||||
useSpecificChatMode,
|
DevDialogContent,
|
||||||
useThreadChat,
|
DevDialogFooter,
|
||||||
} from "@/components/workspace/chats";
|
DevDialogHeader,
|
||||||
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
DevDialogTitle,
|
||||||
|
} from "@/components/ui/dev-dialog";
|
||||||
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
|
import {
|
||||||
|
ArtifactFileDetail,
|
||||||
|
ArtifactFileList,
|
||||||
|
useArtifacts,
|
||||||
|
} from "@/components/workspace/artifacts";
|
||||||
|
import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
||||||
|
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
import { Tooltip } from "@/components/workspace/tooltip";
|
||||||
|
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
||||||
import { Welcome } from "@/components/workspace/welcome";
|
import { Welcome } from "@/components/workspace/welcome";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { bootstrapRemoteSkill } from "@/core/skills";
|
||||||
import { textOfMessage } from "@/core/threads/utils";
|
import { type AgentThread, type AgentThreadState } from "@/core/threads";
|
||||||
|
import { useSubmitThread, useThreadStream } from "@/core/threads/hooks";
|
||||||
|
import {
|
||||||
|
pathOfThread,
|
||||||
|
textOfMessage,
|
||||||
|
titleOfThread,
|
||||||
|
} from "@/core/threads/utils";
|
||||||
|
import { uuid } from "@/core/utils/uuid";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const router = useRouter();
|
||||||
|
|
||||||
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
const [settings, setSettings] = useLocalSettings();
|
||||||
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
const {
|
||||||
|
artifacts,
|
||||||
|
open: artifactsOpen,
|
||||||
|
setOpen: setArtifactsOpen,
|
||||||
|
setArtifacts,
|
||||||
|
select: selectArtifact,
|
||||||
|
selectedArtifact,
|
||||||
|
fullscreen,
|
||||||
|
} = useArtifacts();
|
||||||
|
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const promptInputController = usePromptInputController();
|
||||||
|
|
||||||
|
// UI mode depends only on route: /workspace/chats/new is always "new page" mode.
|
||||||
|
const isNewThread = useMemo(
|
||||||
|
() => threadIdFromPath === "new",
|
||||||
|
[threadIdFromPath],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Submission strategy is controlled by `isnew` query param only.
|
||||||
|
// - isnew=false: reuse existing thread
|
||||||
|
// - otherwise: create/start a new session
|
||||||
|
const createNewSession = useMemo(() => {
|
||||||
|
if (threadIdFromPath !== "new") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchParams.get("isnew")?.trim().toLowerCase() !== "false";
|
||||||
|
}, [threadIdFromPath, searchParams]);
|
||||||
|
|
||||||
|
const uploadTarget = useMemo(() => {
|
||||||
|
const target = searchParams.get("upload_target")?.trim().toLowerCase();
|
||||||
|
return target === "skill" ? "skill" : undefined;
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const [threadId, setThreadId] = useState<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (threadIdFromPath !== "new") {
|
||||||
|
setThreadId(threadIdFromPath);
|
||||||
|
} else {
|
||||||
|
const queryThreadId = searchParams.get("thread_id")?.trim();
|
||||||
|
setThreadId(queryThreadId ?? uuid());
|
||||||
|
}
|
||||||
|
}, [threadIdFromPath, searchParams]);
|
||||||
|
|
||||||
|
// Runtime strategy for /new page:
|
||||||
|
// - UI remains new-page mode
|
||||||
|
// - if isnew=false, execute against existing thread_id without creating a new one
|
||||||
|
const reuseExistingThread = useMemo(
|
||||||
|
() => threadIdFromPath === "new" && !createNewSession && !!threadId,
|
||||||
|
[threadIdFromPath, createNewSession, threadId],
|
||||||
|
);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
// 监听宿主页 selectedSkill 消息
|
||||||
threadId: isNewThread ? undefined : threadId,
|
const {
|
||||||
context: settings.context,
|
selectedSkill,
|
||||||
isMock,
|
skillError: selectedSkillError,
|
||||||
onStart: () => {
|
clearSkillError: clearSelectedSkillError,
|
||||||
setIsNewThread(false);
|
isBootstrapping: isSelectedSkillBootstrapping,
|
||||||
// ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead.
|
} = useSelectedSkillListener({ threadId });
|
||||||
history.replaceState(null, "", `/workspace/chats/${threadId}`);
|
const [finalState, setFinalState] = useState<AgentThreadState | null>(null);
|
||||||
},
|
const thread = useThreadStream({
|
||||||
|
// Keep UI in new-page mode, but runtime may reuse existing thread
|
||||||
|
isNewThread: reuseExistingThread ? false : isNewThread,
|
||||||
|
threadId,
|
||||||
|
fetchStateHistory: true,
|
||||||
onFinish: (state) => {
|
onFinish: (state) => {
|
||||||
|
setFinalState(state);
|
||||||
|
// 新对话完成后导航到对话页面
|
||||||
|
if (isNewThread && threadId) {
|
||||||
|
router.push(pathOfThread(threadId));
|
||||||
|
}
|
||||||
if (document.hidden || !document.hasFocus()) {
|
if (document.hidden || !document.hasFocus()) {
|
||||||
let body = "Conversation finished";
|
let body = "Conversation finished";
|
||||||
const lastMessage = state.messages.at(-1);
|
const lastMessage = state.messages[state.messages.length - 1];
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
const textContent = textOfMessage(lastMessage);
|
const textContent = textOfMessage(lastMessage);
|
||||||
if (textContent) {
|
if (textContent) {
|
||||||
body =
|
if (textContent.length > 200) {
|
||||||
textContent.length > 200
|
body = textContent.substring(0, 200) + "...";
|
||||||
? textContent.substring(0, 200) + "..."
|
} else {
|
||||||
: textContent;
|
body = textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showNotification(state.title, { body });
|
|
||||||
}
|
}
|
||||||
|
showNotification(state.title, {
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) as unknown as UseStream<AgentThreadState>;
|
||||||
|
useEffect(() => {
|
||||||
|
if (thread.isLoading) setFinalState(null);
|
||||||
|
}, [thread.isLoading]);
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
let result = isNewThread
|
||||||
|
? ""
|
||||||
|
: titleOfThread(thread as unknown as AgentThread);
|
||||||
|
if (result === "Untitled") {
|
||||||
|
result = "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [thread, isNewThread]);
|
||||||
|
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
|
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pageTitle = isNewThread
|
||||||
|
? t.pages.newChat
|
||||||
|
: thread.values?.title && thread.values.title !== "Untitled"
|
||||||
|
? thread.values.title
|
||||||
|
: t.pages.untitled;
|
||||||
|
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
|
||||||
|
document.title = `Loading... - ${t.pages.appName}`;
|
||||||
|
} else {
|
||||||
|
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isNewThread,
|
||||||
|
t.pages.newChat,
|
||||||
|
t.pages.untitled,
|
||||||
|
t.pages.appName,
|
||||||
|
thread.values.title,
|
||||||
|
thread.isThreadLoading,
|
||||||
|
suppressExistingThreadPrefetchUi,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
setArtifacts(thread.values.artifacts);
|
||||||
|
if (
|
||||||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||||
|
autoSelectFirstArtifact
|
||||||
|
) {
|
||||||
|
if (thread?.values?.artifacts?.length > 0) {
|
||||||
|
setAutoSelectFirstArtifact(false);
|
||||||
|
selectArtifact(thread.values.artifacts[0]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
autoSelectFirstArtifact,
|
||||||
|
selectArtifact,
|
||||||
|
setArtifacts,
|
||||||
|
thread.values.artifacts,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const artifactPanelOpen = useMemo(() => {
|
||||||
|
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") {
|
||||||
|
return artifactsOpen && artifacts?.length > 0;
|
||||||
|
}
|
||||||
|
return artifactsOpen;
|
||||||
|
}, [artifactsOpen, artifacts]);
|
||||||
|
|
||||||
|
const [todoListCollapsed, setTodoListCollapsed] = useState(true);
|
||||||
|
const [showExitDialog, setShowExitDialog] = useState(false);
|
||||||
|
|
||||||
|
const submitThread = useSubmitThread({
|
||||||
|
isNewThread,
|
||||||
|
createNewSession,
|
||||||
|
threadId,
|
||||||
|
thread,
|
||||||
|
uploadTarget,
|
||||||
|
threadContext: {
|
||||||
|
...settings.context,
|
||||||
|
thinking_enabled: settings.context.mode !== "flash",
|
||||||
|
is_plan_mode:
|
||||||
|
settings.context.mode === "pro" || settings.context.mode === "ultra",
|
||||||
|
subagent_enabled: settings.context.mode === "ultra",
|
||||||
|
},
|
||||||
|
afterSubmit() {
|
||||||
|
// 导航已在 onFinish 中处理,确保 stream 完成后再导航
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(message: PromptInputMessage) => {
|
(message: Parameters<typeof submitThread>[0]) => {
|
||||||
void sendMessage(threadId, message);
|
if (isSelectedSkillBootstrapping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasSubmitted(true);
|
||||||
|
void submitThread(message);
|
||||||
},
|
},
|
||||||
[sendMessage, threadId],
|
[isSelectedSkillBootstrapping, submitThread],
|
||||||
);
|
);
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
|
|
||||||
|
if (!threadId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ thread, isMock }}>
|
<ThreadContext.Provider value={{ threadId, thread }}>
|
||||||
<ChatBox threadId={threadId}>
|
|
||||||
<div className="relative flex size-full min-h-0 justify-between">
|
|
||||||
<header
|
|
||||||
className={cn(
|
|
||||||
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center px-4",
|
|
||||||
isNewThread
|
|
||||||
? "bg-background/0 backdrop-blur-none"
|
|
||||||
: "bg-background/80 shadow-xs backdrop-blur",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center text-sm font-medium">
|
|
||||||
<ThreadTitle threadId={threadId} thread={thread} />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TokenUsageIndicator messages={thread.messages} />
|
|
||||||
<ExportTrigger threadId={threadId} />
|
|
||||||
<ArtifactTrigger />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main className="flex min-h-0 max-w-full grow flex-col">
|
|
||||||
<div className="flex size-full justify-center">
|
|
||||||
<MessageList
|
|
||||||
className={cn("size-full", !isNewThread && "pt-10")}
|
|
||||||
threadId={threadId}
|
|
||||||
thread={thread}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full",
|
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
||||||
isNewThread && "-translate-y-[calc(50vh-96px)]",
|
artifactsOpen ? "w-full" : "w-[70%]",
|
||||||
isNewThread
|
|
||||||
? "max-w-(--container-width-sm)"
|
|
||||||
: "max-w-(--container-width-md)",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute -top-4 right-0 left-0 z-0">
|
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||||
<div className="absolute right-0 bottom-0 left-0">
|
<div
|
||||||
<TodoList
|
className={cn(
|
||||||
className="bg-background/5"
|
"relative overflow-hidden rounded-t-[20px] transition-all duration-300 ease-in-out",
|
||||||
|
artifactPanelOpen ? "w-[50%]" : "w-full",
|
||||||
|
fullscreen && "hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||||
|
<header
|
||||||
|
className={cn(
|
||||||
|
"bg-background absolute top-0 right-0 left-0 z-30 mx-4 grid h-[58px] shrink-0 grid-cols-3 items-center border-b transition-all duration-300 ease-in-out",
|
||||||
|
isNewThread && !hasSubmitted ? "hidden" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-start overflow-hidden text-sm font-medium">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="px-[10px] py-[5px] text-sm font-medium text-[#150033] hover:text-[#150033]/80"
|
||||||
|
onClick={() => setShowExitDialog(true)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.5 10H13.25H15.6875H16.5M3.5 10L7.5625 6M3.5 10L7.5625 14"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center overflow-hidden text-sm font-bold font-medium whitespace-nowrap text-[#333333]">
|
||||||
|
{title !== "Untitled" && (
|
||||||
|
<ThreadTitle threadId={threadId} threadTitle={title} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2 overflow-hidden">
|
||||||
|
<DevTodoList
|
||||||
|
className="bg-white"
|
||||||
todos={thread.values.todos ?? []}
|
todos={thread.values.todos ?? []}
|
||||||
hidden={
|
hidden={
|
||||||
!thread.values.todos || thread.values.todos.length === 0
|
!thread.values.todos || thread.values.todos.length === 0
|
||||||
}
|
}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-full px-[10px] py-[5px] text-sm font-medium text-[#150033] hover:text-[#150033]"
|
||||||
|
>
|
||||||
|
<ListTodoIcon className="size-4" /> To-dos
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{artifacts?.length > 0 && !artifactsOpen && (
|
||||||
|
<Tooltip content="点击可查看生成的文件结果">
|
||||||
|
<Button
|
||||||
|
className="text-[#150033] hover:text-[#150033]/80"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setArtifactsOpen(true);
|
||||||
|
setSidebarOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilesIcon />
|
||||||
|
{t.common.artifacts}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-0 max-w-full grow flex-col",
|
||||||
|
isNewThread && !hasSubmitted ? "bg-white" : "bg-background",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex size-full justify-center">
|
||||||
|
<MessageList
|
||||||
|
className={cn(
|
||||||
|
"size-full",
|
||||||
|
(!isNewThread || hasSubmitted) && "pt-[20px]",
|
||||||
|
)}
|
||||||
|
threadId={threadId}
|
||||||
|
thread={thread}
|
||||||
|
suppressThreadLoading={suppressExistingThreadPrefetchUi}
|
||||||
|
messagesOverride={
|
||||||
|
suppressExistingThreadPrefetchUi
|
||||||
|
? []
|
||||||
|
: !thread.isLoading && finalState?.messages
|
||||||
|
? (finalState.messages as Message[])
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<InputBox
|
</div>
|
||||||
className={cn("bg-background/5 w-full -translate-y-4")}
|
<div
|
||||||
isNewThread={isNewThread}
|
className={cn(
|
||||||
|
"bg-background ml-[20px] rounded-t-[20px] transition-all duration-300 ease-in-out",
|
||||||
|
!artifactsOpen && "opacity-0",
|
||||||
|
artifactPanelOpen
|
||||||
|
? fullscreen
|
||||||
|
? "ml-0 w-full"
|
||||||
|
: "w-[50%]"
|
||||||
|
: "w-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full transition-transform duration-300 ease-in-out",
|
||||||
|
artifactPanelOpen ? "translate-x-0" : "translate-x-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedArtifact ? (
|
||||||
|
<ArtifactFileDetail
|
||||||
|
className="size-full"
|
||||||
|
filepath={selectedArtifact}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative flex size-full justify-center px-[20px]">
|
||||||
|
<div className="absolute top-2 right-2 z-30">
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setArtifactsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{thread.values.artifacts?.length === 0 ? (
|
||||||
|
<ConversationEmptyState
|
||||||
|
icon={<FilesIcon />}
|
||||||
|
title="No artifact selected"
|
||||||
|
description="Select an artifact to view its details"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4">
|
||||||
|
<header className="shrink-0">
|
||||||
|
<h2 className="text-[14px] font-bold text-[#333333]">
|
||||||
|
{t.common.artifacts}
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<main className="min-h-0 grow">
|
||||||
|
<ArtifactFileList
|
||||||
|
className="max-w-(--container-width-sm) p-4 pt-12"
|
||||||
|
files={thread.values.artifacts ?? []}
|
||||||
|
threadId={threadId}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed 底部居中输入框容器 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
|
||||||
|
"transition-all duration-300 ease-in-out",
|
||||||
|
fullscreen ? "hidden" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-auto relative w-full max-w-[720px]",
|
||||||
|
isNewThread && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InputBox
|
||||||
|
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
||||||
|
isNewThread={isNewThread}
|
||||||
|
hasSubmitted={hasSubmitted}
|
||||||
autoFocus={isNewThread}
|
autoFocus={isNewThread}
|
||||||
status={
|
status={
|
||||||
thread.error
|
suppressExistingThreadPrefetchUi
|
||||||
? "error"
|
? "ready"
|
||||||
: thread.isLoading
|
: thread.isLoading
|
||||||
? "streaming"
|
? "streaming"
|
||||||
: "ready"
|
: "ready"
|
||||||
}
|
}
|
||||||
context={settings.context}
|
context={settings.context}
|
||||||
extraHeader={
|
extraHeader={
|
||||||
isNewThread && <Welcome mode={settings.context.mode} />
|
<div className="flex flex-col gap-4">
|
||||||
|
{isNewThread && !hasSubmitted && (
|
||||||
|
<Welcome mode={settings.context.mode} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||||
|
isSelectedSkillBootstrapping
|
||||||
}
|
}
|
||||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading}
|
|
||||||
onContextChange={(context) => setSettings("context", context)}
|
onContextChange={(context) => setSettings("context", context)}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* {isSelectedSkillBootstrapping && (
|
||||||
|
<div className="text-muted-foreground w-full translate-y-8 text-center text-xs">
|
||||||
|
正在初始化 Skill 文件...
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
||||||
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
||||||
{t.common.notAvailableInDemoMode}
|
{t.common.notAvailableInDemoMode}
|
||||||
|
|
@ -149,9 +484,74 @@ export default function ChatPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
{/* 退出确认对话框 */}
|
||||||
|
<DevDialog open={showExitDialog} onOpenChange={setShowExitDialog}>
|
||||||
|
<DevDialogContent>
|
||||||
|
<DevDialogHeader>
|
||||||
|
<DevDialogTitle>提示</DevDialogTitle>
|
||||||
|
</DevDialogHeader>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
退出后,当前会话结束并销毁,请先下载保存当前结果!
|
||||||
|
</p>
|
||||||
|
<DevDialogFooter>
|
||||||
|
<Button
|
||||||
|
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowExitDialog(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={async () => {
|
||||||
|
// 如果正在生成,先终止再退出
|
||||||
|
if (thread.isLoading) {
|
||||||
|
await handleStop();
|
||||||
|
}
|
||||||
|
setShowExitDialog(false);
|
||||||
|
// 使用完整页面刷新确保组件重新挂载,isNewThread 为 true
|
||||||
|
window.location.href = "/workspace/chats/new";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</DevDialogFooter>
|
||||||
|
</DevDialogContent>
|
||||||
|
</DevDialog>
|
||||||
|
|
||||||
|
{/* selectedSkill 失败:错误弹窗 */}
|
||||||
|
<DevDialog
|
||||||
|
open={!!selectedSkillError}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) clearSelectedSkillError();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DevDialogContent>
|
||||||
|
<DevDialogHeader>
|
||||||
|
<DevDialogTitle>
|
||||||
|
⚠️ {selectedSkillError?.title ?? "技能加载失败"}
|
||||||
|
</DevDialogTitle>
|
||||||
|
</DevDialogHeader>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{selectedSkillError?.message ?? "发生了未知错误,请稍后重试。"}
|
||||||
|
</p>
|
||||||
|
<DevDialogFooter singleColumn>
|
||||||
|
<Button
|
||||||
|
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={clearSelectedSkillError}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DevDialogFooter>
|
||||||
|
</DevDialogContent>
|
||||||
|
</DevDialog>
|
||||||
|
|
||||||
|
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||||
|
{/* <IframeTestPanel /> */}
|
||||||
</div>
|
</div>
|
||||||
</ChatBox>
|
|
||||||
</ThreadContext.Provider>
|
</ThreadContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||||
import { Toaster } from "sonner";
|
|
||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { CommandPalette } from "@/components/workspace/command-palette";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
|
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
|
||||||
import { getLocalSettings, useLocalSettings } from "@/core/settings";
|
import { getLocalSettings, useLocalSettings } from "@/core/settings";
|
||||||
|
|
||||||
|
|
@ -16,6 +16,11 @@ export default function WorkspaceLayout({
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
const [settings, setSettings] = useLocalSettings();
|
const [settings, setSettings] = useLocalSettings();
|
||||||
const [open, setOpen] = useState(false); // SSR default: open (matches server render)
|
const [open, setOpen] = useState(false); // SSR default: open (matches server render)
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// iframe 技能模式(mode=skill)时隐藏侧边栏
|
||||||
|
const isSkillMode = searchParams.get("mode") === "skill";
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Runs synchronously before first paint on the client — no visual flash
|
// Runs synchronously before first paint on the client — no visual flash
|
||||||
setOpen(!getLocalSettings().layout.sidebar_collapsed);
|
setOpen(!getLocalSettings().layout.sidebar_collapsed);
|
||||||
|
|
@ -37,11 +42,38 @@ export default function WorkspaceLayout({
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
<WorkspaceSidebar />
|
{/* MARK:!!!! 生产环境下必须注释才能提交!!!! */}
|
||||||
|
{/* {!isSkillMode && <WorkspaceSidebar className="" />} */}
|
||||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<CommandPalette />
|
<Toaster
|
||||||
<Toaster position="top-center" />
|
position="top-center"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 2200,
|
||||||
|
classNames: {
|
||||||
|
toast: [
|
||||||
|
/* 灰色圆角矩形容器 */
|
||||||
|
"rounded-[20px] border-none",
|
||||||
|
/* 浅灰色背景 + 轻微透明 */
|
||||||
|
"bg-[#999999]! backdrop-blur-sm",
|
||||||
|
/* 阴影极轻 */
|
||||||
|
"shadow-[0_2px_12px_0_rgba(0,0,0,0.18)]",
|
||||||
|
/* 内边距:宽松居中 */
|
||||||
|
"px-5 py-2.5",
|
||||||
|
/* 单行布局,内容水平居中 */
|
||||||
|
"flex items-center justify-center gap-0",
|
||||||
|
/* 整体文字样式 */
|
||||||
|
"text-white text-sm font-normal font-sans",
|
||||||
|
/* 去掉 icon 区域间距 */
|
||||||
|
"[&>[data-icon]]:hidden",
|
||||||
|
].join(" "),
|
||||||
|
title:
|
||||||
|
"text-white! text-sm font-normal text-center w-full leading-snug",
|
||||||
|
description: "hidden",
|
||||||
|
icon: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
||||||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background flex flex-col overflow-hidden rounded-lg border shadow-lg",
|
"bg-background flex flex-col overflow-hidden rounded-t-[20px] px-[20px] pt-[15px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -30,10 +30,7 @@ export const ArtifactHeader = ({
|
||||||
...props
|
...props
|
||||||
}: ArtifactHeaderProps) => (
|
}: ArtifactHeaderProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("mb-[10px] flex items-center justify-between", className)}
|
||||||
"bg-muted/50 flex items-center justify-between border-b px-4 py-3",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -66,7 +63,7 @@ export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
||||||
|
|
||||||
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn("text-foreground text-sm font-medium", className)}
|
className={cn("text-foreground flex justify-center w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -93,6 +90,7 @@ export type ArtifactActionProps = ComponentProps<typeof Button> & {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
asChild?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ArtifactAction = ({
|
export const ArtifactAction = ({
|
||||||
|
|
@ -103,6 +101,7 @@ export const ArtifactAction = ({
|
||||||
className,
|
className,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
variant = "ghost",
|
variant = "ghost",
|
||||||
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: ArtifactActionProps) => {
|
}: ArtifactActionProps) => {
|
||||||
const button = (
|
const button = (
|
||||||
|
|
@ -114,6 +113,7 @@ export const ArtifactAction = ({
|
||||||
size={size}
|
size={size}
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
asChild={asChild}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon className="size-4" /> : children}
|
{Icon ? <Icon className="size-4" /> : children}
|
||||||
|
|
@ -143,8 +143,7 @@ export const ArtifactContent = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtifactContentProps) => (
|
}: ArtifactContentProps) => (
|
||||||
<div
|
<div className="min-h-0 rounded-[10px] flex-1 overflow-auto">
|
||||||
className={cn("min-h-0 flex-1 overflow-auto p-4", className)}
|
<div className={cn("mb-[150px] p-4", className)} {...props} />
|
||||||
{...props}
|
</div>
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,11 @@ export const ChainOfThoughtStep = memo(
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative mt-0.5">
|
<div className="relative mt-0.5">
|
||||||
{isValidElement(Icon) ? Icon : <Icon className="size-4" />}
|
{isValidElement(Icon) ? (
|
||||||
|
Icon
|
||||||
|
) : (
|
||||||
|
<Icon className="size-4 stroke-[#333333] stroke-[1.5px] text-[#333333]" />
|
||||||
|
)}
|
||||||
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
|
<div className="bg-border absolute top-7 bottom-0 left-1/2 -mx-px w-px" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-2 overflow-hidden">
|
<div className="flex-1 space-y-2 overflow-hidden">
|
||||||
|
|
@ -202,7 +206,7 @@ export const ChainOfThoughtContent = memo(
|
||||||
<Collapsible open={isOpen}>
|
<Collapsible open={isOpen}>
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 space-y-3",
|
"mt-2 space-y-3 bg-[#ffffff]",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ export const Checkpoint = ({
|
||||||
...props
|
...props
|
||||||
}: CheckpointProps) => (
|
}: CheckpointProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center gap-0.5 text-muted-foreground overflow-hidden", className)}
|
className={cn(
|
||||||
|
"text-muted-foreground flex items-center gap-0.5 overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, copyToClipboard } from "@/lib/utils";
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
|
|
@ -146,14 +146,9 @@ export const CodeBlockCopyButton = ({
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
const { code } = useContext(CodeBlockContext);
|
const { code } = useContext(CodeBlockContext);
|
||||||
|
|
||||||
const copyToClipboard = async () => {
|
const handleCopy = async () => {
|
||||||
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
|
||||||
onError?.(new Error("Clipboard API not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(code);
|
await copyToClipboard(code);
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
onCopy?.();
|
onCopy?.();
|
||||||
setTimeout(() => setIsCopied(false), timeout);
|
setTimeout(() => setIsCopied(false), timeout);
|
||||||
|
|
@ -167,7 +162,7 @@ export const CodeBlockCopyButton = ({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
onClick={copyToClipboard}
|
onClick={handleCopy}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<Button type="button" variant="ghost" {...props}>
|
<Button type="button" variant="ghost" {...props}>
|
||||||
<span className="font-medium text-muted-foreground">
|
<span className="text-muted-foreground font-medium">
|
||||||
{renderedPercent}
|
{renderedPercent}
|
||||||
</span>
|
</span>
|
||||||
<ContextIcon />
|
<ContextIcon />
|
||||||
|
|
@ -163,7 +163,7 @@ export const ContextContentHeader = ({
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-3 text-xs">
|
<div className="flex items-center justify-between gap-3 text-xs">
|
||||||
<p>{displayPct}</p>
|
<p>{displayPct}</p>
|
||||||
<p className="font-mono text-muted-foreground">
|
<p className="text-muted-foreground font-mono">
|
||||||
{used} / {total}
|
{used} / {total}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -213,8 +213,8 @@ export const ContextContentFooter = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
|
"bg-secondary flex w-full items-center justify-between gap-3 p-3 text-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -402,7 +402,7 @@ const TokensWithCost = ({
|
||||||
notation: "compact",
|
notation: "compact",
|
||||||
}).format(tokens)}
|
}).format(tokens)}
|
||||||
{costText ? (
|
{costText ? (
|
||||||
<span className="ml-2 text-muted-foreground">• {costText}</span>
|
<span className="text-muted-foreground ml-2">• {costText}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
||||||
export const Controls = ({ className, ...props }: ControlsProps) => (
|
export const Controls = ({ className, ...props }: ControlsProps) => (
|
||||||
<ControlsPrimitive
|
<ControlsPrimitive
|
||||||
className={cn(
|
className={cn(
|
||||||
"gap-px overflow-hidden rounded-md border bg-card p-1 shadow-none!",
|
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
|
||||||
"[&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!",
|
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
<StickToBottom
|
<StickToBottom
|
||||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
className={cn("relative mt-[60px] flex-1 overflow-y-hidden", className)}
|
||||||
initial="smooth"
|
initial="smooth"
|
||||||
resize="smooth"
|
resize="smooth"
|
||||||
role="log"
|
role="log"
|
||||||
|
|
@ -28,7 +28,7 @@ export const ConversationContent = ({
|
||||||
...props
|
...props
|
||||||
}: ConversationContentProps) => (
|
}: ConversationContentProps) => (
|
||||||
<StickToBottom.Content
|
<StickToBottom.Content
|
||||||
className={cn("flex flex-col gap-8 p-4", className)}
|
className={cn("flex flex-col gap-8 p-[20px]", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const Temporary = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
className="stroke-1 stroke-ring"
|
className="stroke-ring stroke-1"
|
||||||
id={id}
|
id={id}
|
||||||
path={edgePath}
|
path={edgePath}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -41,13 +41,13 @@ const Temporary = ({
|
||||||
|
|
||||||
const getHandleCoordsByPosition = (
|
const getHandleCoordsByPosition = (
|
||||||
node: InternalNode<Node>,
|
node: InternalNode<Node>,
|
||||||
handlePosition: Position
|
handlePosition: Position,
|
||||||
) => {
|
) => {
|
||||||
// Choose the handle type based on position - Left is for target, Right is for source
|
// Choose the handle type based on position - Left is for target, Right is for source
|
||||||
const handleType = handlePosition === Position.Left ? "target" : "source";
|
const handleType = handlePosition === Position.Left ? "target" : "source";
|
||||||
|
|
||||||
const handle = node.internals.handleBounds?.[handleType]?.find(
|
const handle = node.internals.handleBounds?.[handleType]?.find(
|
||||||
(h) => h.position === handlePosition
|
(h) => h.position === handlePosition,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
|
|
@ -85,7 +85,7 @@ const getHandleCoordsByPosition = (
|
||||||
|
|
||||||
const getEdgeParams = (
|
const getEdgeParams = (
|
||||||
source: InternalNode<Node>,
|
source: InternalNode<Node>,
|
||||||
target: InternalNode<Node>
|
target: InternalNode<Node>,
|
||||||
) => {
|
) => {
|
||||||
const sourcePos = Position.Right;
|
const sourcePos = Position.Right;
|
||||||
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
|
||||||
|
|
@ -112,7 +112,7 @@ const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
|
||||||
|
|
||||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
||||||
sourceNode,
|
sourceNode,
|
||||||
targetNode
|
targetNode,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [edgePath] = getBezierPath({
|
const [edgePath] = getBezierPath({
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const Image = ({
|
||||||
alt={props.alt}
|
alt={props.alt}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto max-w-full overflow-hidden rounded-md",
|
"h-auto max-w-full overflow-hidden rounded-md",
|
||||||
props.className
|
props.className,
|
||||||
)}
|
)}
|
||||||
src={`data:${mediaType};base64,${base64}`}
|
src={`data:${mediaType};base64,${base64}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex animate-spin items-center justify-center",
|
"inline-flex animate-spin items-center justify-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { FileUIPart, UIMessage } from "ai";
|
import type { FileUIPart, UIMessage } from "ai";
|
||||||
import {
|
import {
|
||||||
|
|
@ -27,8 +28,10 @@ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
export const Message = ({ className, from, ...props }: MessageProps) => (
|
export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex w-full flex-col gap-2",
|
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
||||||
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
from === "user"
|
||||||
|
? "is-user ml-auto justify-end px-0 pb-0"
|
||||||
|
: "is-assistant bg-[#ffffff]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -331,6 +334,7 @@ export function MessageAttachment({
|
||||||
onRemove,
|
onRemove,
|
||||||
...props
|
...props
|
||||||
}: MessageAttachmentProps) {
|
}: MessageAttachmentProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
const filename = data.filename || "";
|
const filename = data.filename || "";
|
||||||
const mediaType =
|
const mediaType =
|
||||||
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
||||||
|
|
@ -356,7 +360,7 @@ export function MessageAttachment({
|
||||||
/>
|
/>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Remove attachment"
|
aria-label={t.common.removeAttachment}
|
||||||
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
className="bg-background/80 hover:bg-background absolute top-2 right-2 size-6 rounded-full p-0 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -366,7 +370,7 @@ export function MessageAttachment({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">{t.common.removeAttachment}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -384,7 +388,7 @@ export function MessageAttachment({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Remove attachment"
|
aria-label={t.common.removeAttachment}
|
||||||
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
className="hover:bg-accent size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity group-hover:opacity-100 [&>svg]:size-3"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -394,6 +398,7 @@ export function MessageAttachment({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
|
<span className="sr-only">{t.common.removeAttachment}</span>
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const Node = ({ handles, className, ...props }: NodeProps) => (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
"node-container relative size-full h-auto w-sm gap-0 rounded-md p-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -36,7 +36,7 @@ export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
||||||
|
|
||||||
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className={cn("gap-0.5 rounded-t-md border-b bg-secondary p-3!", className)}
|
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -65,7 +65,7 @@ export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
||||||
|
|
||||||
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
||||||
<CardFooter
|
<CardFooter
|
||||||
className={cn("rounded-b-md border-t bg-secondary p-3!", className)}
|
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ type PanelProps = ComponentProps<typeof PanelPrimitive>;
|
||||||
export const Panel = ({ className, ...props }: PanelProps) => (
|
export const Panel = ({ className, ...props }: PanelProps) => (
|
||||||
<PanelPrimitive
|
<PanelPrimitive
|
||||||
className={cn(
|
className={cn(
|
||||||
"m-4 overflow-hidden rounded-md border bg-card p-1",
|
"bg-card m-4 overflow-hidden rounded-md border p-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Tooltip } from "../workspace/tooltip";
|
||||||
import type { ChatStatus, FileUIPart } from "ai";
|
import type { ChatStatus, FileUIPart } from "ai";
|
||||||
import {
|
import {
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
|
|
@ -70,6 +71,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Provider Context & Types
|
// Provider Context & Types
|
||||||
|
|
@ -288,6 +290,7 @@ export function PromptInputAttachment({
|
||||||
...props
|
...props
|
||||||
}: PromptInputAttachmentProps) {
|
}: PromptInputAttachmentProps) {
|
||||||
const attachments = usePromptInputAttachments();
|
const attachments = usePromptInputAttachments();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const filename = data.filename || "";
|
const filename = data.filename || "";
|
||||||
|
|
||||||
|
|
@ -295,81 +298,112 @@ export function PromptInputAttachment({
|
||||||
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
|
||||||
const isImage = mediaType === "image";
|
const isImage = mediaType === "image";
|
||||||
|
|
||||||
const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
|
const truncateFilename = (name: string, maxLen: number = 10) => {
|
||||||
|
if (name.length <= maxLen) return name;
|
||||||
|
const ext = name.slice(name.lastIndexOf("."));
|
||||||
|
const baseName = name.slice(0, name.lastIndexOf("."));
|
||||||
|
const truncated = baseName.slice(0, maxLen - ext.length - 3);
|
||||||
|
return truncated + "..." + ext;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromptInputHoverCard>
|
|
||||||
<HoverCardTrigger asChild>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group border-border hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 relative flex h-8 cursor-pointer items-center gap-1.5 rounded-md border px-1.5 text-sm font-medium transition-all select-none",
|
"group relative flex size-16 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-lg transition-all select-none",
|
||||||
|
isImage ? "p-0" : "bg-gray-100 dark:bg-gray-700",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
key={data.id}
|
key={data.id}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="relative size-5 shrink-0">
|
|
||||||
<div className="bg-background absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded transition-opacity group-hover:opacity-0">
|
|
||||||
{isImage ? (
|
{isImage ? (
|
||||||
|
<>
|
||||||
<img
|
<img
|
||||||
alt={filename || "attachment"}
|
alt={filename || "attachment"}
|
||||||
className="size-5 object-cover"
|
className="size-full object-cover"
|
||||||
height={20}
|
|
||||||
src={data.url}
|
src={data.url}
|
||||||
width={20}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
{/* 悬浮遮罩层 */}
|
||||||
<div className="text-muted-foreground flex size-5 items-center justify-center">
|
<div
|
||||||
<PaperclipIcon className="size-3" />
|
className="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
</div>
|
style={{ borderRadius: "10px", background: "rgba(0, 0, 0, 0.60)" }}
|
||||||
)}
|
>
|
||||||
</div>
|
{/* 眼睛图标 - 居中 */}
|
||||||
<Button
|
<svg
|
||||||
aria-label="Remove attachment"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5"
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 4.75C13.3315 4.75 16.4669 6.61444 18.9805 9.88281C19.0335 9.95183 19.0335 10.0482 18.9805 10.1172C16.4669 13.3856 13.3315 15.25 10 15.25C6.66835 15.2499 3.53309 13.3857 1.01953 10.1172C0.966466 10.0482 0.966465 9.95182 1.01953 9.88281C3.53309 6.61435 6.66835 4.75014 10 4.75Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10 7.75C11.2426 7.75 12.25 8.75736 12.25 10C12.25 11.2426 11.2426 12.25 10 12.25C8.75736 12.25 7.75 11.2426 7.75 10C7.75 8.75736 8.75736 7.75 10 7.75Z"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/* 删除按钮 - 右上角 */}
|
||||||
|
<button
|
||||||
|
aria-label={t.common.removeAttachment}
|
||||||
|
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-white/20"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
attachments.remove(data.id);
|
attachments.remove(data.id);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
|
||||||
>
|
>
|
||||||
<XIcon />
|
<svg
|
||||||
<span className="sr-only">Remove</span>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</Button>
|
width="8"
|
||||||
</div>
|
height="8"
|
||||||
|
viewBox="0 0 8 8"
|
||||||
<span className="flex-1 truncate">{attachmentLabel}</span>
|
fill="none"
|
||||||
</div>
|
>
|
||||||
</HoverCardTrigger>
|
<path
|
||||||
<PromptInputHoverCardContent className="w-auto p-2">
|
d="M0.75 0.75L6.74995 6.74995"
|
||||||
<div className="w-auto space-y-3">
|
stroke="white"
|
||||||
{isImage && (
|
strokeWidth="1.5"
|
||||||
<div className="flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border">
|
strokeLinecap="round"
|
||||||
<img
|
|
||||||
alt={filename || "attachment preview"}
|
|
||||||
className="max-h-full max-w-full object-contain"
|
|
||||||
height={384}
|
|
||||||
src={data.url}
|
|
||||||
width={448}
|
|
||||||
/>
|
/>
|
||||||
|
<path
|
||||||
|
d="M6.75 0.75L0.750025 6.74992"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
<div className="flex items-center gap-2.5">
|
) : (
|
||||||
<div className="min-w-0 flex-1 space-y-1 px-0.5">
|
<>
|
||||||
<h4 className="truncate text-sm leading-none font-semibold">
|
<div className="flex flex-col items-center justify-center gap-1 px-1">
|
||||||
{filename || (isImage ? "Image" : "Attachment")}
|
<PaperclipIcon className="size-6 text-gray-400" />
|
||||||
</h4>
|
<span className="max-w-full truncate text-center text-[10px] text-gray-500">
|
||||||
{data.mediaType && (
|
{truncateFilename(filename)}
|
||||||
<p className="text-muted-foreground truncate font-mono text-xs">
|
</span>
|
||||||
{data.mediaType}
|
</div>
|
||||||
</p>
|
{/* 关闭按钮 - 右上角 */}
|
||||||
|
<button
|
||||||
|
aria-label={t.common.removeAttachment}
|
||||||
|
className="absolute top-1 right-1 z-10 flex size-5 cursor-pointer items-center justify-center rounded bg-white/90 opacity-0 transition-opacity hover:bg-white group-hover:opacity-100 dark:bg-gray-800/90 dark:hover:bg-gray-800"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
attachments.remove(data.id);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<XIcon className="size-3 text-gray-600 dark:text-gray-300" />
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PromptInputHoverCardContent>
|
|
||||||
</PromptInputHoverCard>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,13 +427,14 @@ export function PromptInputAttachments({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
|
className={cn(
|
||||||
|
"inline-flex flex-row flex-nowrap items-center gap-2 rounded-xl p-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{attachments.files.map((file) => (
|
{attachments.files.map((file) => (
|
||||||
<Fragment key={file.id}>
|
<Fragment key={file.id}>{children(file)}</Fragment>
|
||||||
<div className="max-w-60">{children(file)}</div>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -457,10 +492,13 @@ export type PromptInputProps = Omit<
|
||||||
message: PromptInputMessage,
|
message: PromptInputMessage,
|
||||||
event: FormEvent<HTMLFormElement>,
|
event: FormEvent<HTMLFormElement>,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
// className for InputGroup (passes through to inner InputGroup component)
|
||||||
|
inputGroupClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PromptInput = ({
|
export const PromptInput = ({
|
||||||
className,
|
className,
|
||||||
|
inputGroupClassName,
|
||||||
accept,
|
accept,
|
||||||
disabled,
|
disabled,
|
||||||
multiple,
|
multiple,
|
||||||
|
|
@ -794,7 +832,7 @@ export const PromptInput = ({
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<InputGroup>{children}</InputGroup>
|
<InputGroup className={inputGroupClassName}>{children}</InputGroup>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -1027,32 +1065,63 @@ export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
|
||||||
export const PromptInputSubmit = ({
|
export const PromptInputSubmit = ({
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "icon-sm",
|
size = "sm",
|
||||||
status,
|
status,
|
||||||
|
disabled,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: PromptInputSubmitProps) => {
|
}: PromptInputSubmitProps) => {
|
||||||
|
const controller = useOptionalPromptInputController();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// 判断是否有内容可发送
|
||||||
|
const hasContent = controller
|
||||||
|
? controller.textInput.value.trim().length > 0 ||
|
||||||
|
controller.attachments.files.length > 0
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// 正在 streaming 时不允许发送
|
||||||
|
// const isStreaming = status === "streaming" || status === "submitted";
|
||||||
|
|
||||||
|
// const isDisabled = disabled || !hasContent || isStreaming;
|
||||||
|
|
||||||
let Icon = <ArrowUpIcon className="size-4" />;
|
let Icon = <ArrowUpIcon className="size-4" />;
|
||||||
|
|
||||||
|
let text: string = "发送";
|
||||||
|
|
||||||
if (status === "submitted") {
|
if (status === "submitted") {
|
||||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||||
|
text = "生成中...";
|
||||||
} else if (status === "streaming") {
|
} else if (status === "streaming") {
|
||||||
Icon = <SquareIcon className="size-4" />;
|
Icon = <SquareIcon className="size-4" />;
|
||||||
|
text = "停止";
|
||||||
} else if (status === "error") {
|
} else if (status === "error") {
|
||||||
Icon = <XIcon className="size-4" />;
|
Icon = <XIcon className="size-4" />;
|
||||||
|
text = "错误";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Tooltip content={t.inputBox.sendMessagePrice}>
|
||||||
<InputGroupButton
|
<InputGroupButton
|
||||||
aria-label="Submit"
|
aria-label="Submit"
|
||||||
className={cn(className)}
|
// 被button{bgc:#fff}覆盖了,只能加"!"
|
||||||
|
className={cn(
|
||||||
|
"h-[40px] w-[140px] rounded-[10px] border-0 font-bold transition-all",
|
||||||
|
// isDisabled
|
||||||
|
// ? "cursor-not-allowed !bg-gray-200 text-gray-400":
|
||||||
|
"!bg-[#F0E8FB] text-[#8E47F0] hover:!bg-[#8E47F0] hover:text-[#FFFFFF]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
size={size}
|
size={size}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
// disabled={isDisabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? Icon}
|
{/* {children ?? Icon} */}
|
||||||
|
{text}
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1128,8 +1197,6 @@ export const PromptInputSpeechButton = ({
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||||
const callbacksRef = useRef({ textareaRef, onTranscriptionChange });
|
|
||||||
callbacksRef.current = { textareaRef, onTranscriptionChange };
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|
@ -1162,18 +1229,15 @@ export const PromptInputSpeechButton = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTextareaRef = callbacksRef.current.textareaRef;
|
if (finalTranscript && textareaRef?.current) {
|
||||||
const currentOnTranscriptionChange = callbacksRef.current.onTranscriptionChange;
|
const textarea = textareaRef.current;
|
||||||
|
|
||||||
if (finalTranscript && currentTextareaRef?.current) {
|
|
||||||
const textarea = currentTextareaRef.current;
|
|
||||||
const currentValue = textarea.value;
|
const currentValue = textarea.value;
|
||||||
const newValue =
|
const newValue =
|
||||||
currentValue + (currentValue ? " " : "") + finalTranscript;
|
currentValue + (currentValue ? " " : "") + finalTranscript;
|
||||||
|
|
||||||
textarea.value = newValue;
|
textarea.value = newValue;
|
||||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
currentOnTranscriptionChange?.(newValue);
|
onTranscriptionChange?.(newValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1191,7 +1255,7 @@ export const PromptInputSpeechButton = ({
|
||||||
recognitionRef.current.stop();
|
recognitionRef.current.stop();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [textareaRef, onTranscriptionChange]);
|
||||||
|
|
||||||
const toggleListening = useCallback(() => {
|
const toggleListening = useCallback(() => {
|
||||||
if (!recognition) {
|
if (!recognition) {
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ export type QueueItemProps = ComponentProps<"li">;
|
||||||
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
export const QueueItem = ({ className, ...props }: QueueItemProps) => (
|
||||||
<li
|
<li
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors hover:bg-muted",
|
"group hover:bg-muted flex flex-col gap-1 rounded-md px-3 py-1 text-sm transition-colors",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -58,7 +58,7 @@ export const QueueItemIndicator = ({
|
||||||
completed
|
completed
|
||||||
? "border-muted-foreground/20 bg-muted-foreground/10"
|
? "border-muted-foreground/20 bg-muted-foreground/10"
|
||||||
: "border-muted-foreground/50",
|
: "border-muted-foreground/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -79,7 +79,7 @@ export const QueueItemContent = ({
|
||||||
completed
|
completed
|
||||||
? "text-muted-foreground/50 line-through"
|
? "text-muted-foreground/50 line-through"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -100,7 +100,7 @@ export const QueueItemDescription = ({
|
||||||
completed
|
completed
|
||||||
? "text-muted-foreground/40 line-through"
|
? "text-muted-foreground/40 line-through"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -126,8 +126,8 @@ export const QueueItemAction = ({
|
||||||
}: QueueItemActionProps) => (
|
}: QueueItemActionProps) => (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-auto rounded p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted-foreground/10 hover:text-foreground group-hover:opacity-100",
|
"text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground size-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -169,8 +169,8 @@ export const QueueItemFile = ({
|
||||||
}: QueueItemFileProps) => (
|
}: QueueItemFileProps) => (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 rounded border bg-muted px-2 py-1 text-xs",
|
"bg-muted flex items-center gap-1 rounded border px-2 py-1 text-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -186,8 +186,8 @@ export const QueueList = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: QueueListProps) => (
|
}: QueueListProps) => (
|
||||||
<ScrollArea className={cn("-mb-1 mt-2", className)} {...props}>
|
<ScrollArea className={cn("-mb-1", className)} {...props}>
|
||||||
<div className="max-h-40 pr-4">
|
<div className="max-h-40">
|
||||||
<ul>{children}</ul>
|
<ul>{children}</ul>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
@ -215,8 +215,8 @@ export const QueueSectionTrigger = ({
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left font-medium text-muted-foreground text-sm transition-colors hover:bg-muted",
|
"group bg-muted/40 text-muted-foreground hover:bg-muted flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm font-medium transition-colors",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -241,7 +241,7 @@ export const QueueSectionLabel = ({
|
||||||
...props
|
...props
|
||||||
}: QueueSectionLabelProps) => (
|
}: QueueSectionLabelProps) => (
|
||||||
<span className={cn("flex items-center gap-2", className)} {...props}>
|
<span className={cn("flex items-center gap-2", className)} {...props}>
|
||||||
<ChevronDownIcon className="group-data-[state=closed]:-rotate-90 size-4 transition-transform" />
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=closed]:-rotate-90" />
|
||||||
{icon}
|
{icon}
|
||||||
<span>
|
<span>
|
||||||
{count} {label}
|
{count} {label}
|
||||||
|
|
@ -266,8 +266,8 @@ export type QueueProps = ComponentProps<"div">;
|
||||||
export const Queue = ({ className, ...props }: QueueProps) => (
|
export const Queue = ({ className, ...props }: QueueProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col gap-2 rounded-xl border border-border bg-background px-3 pt-2 pb-2 shadow-xs",
|
"border-border bg-background flex flex-col gap-2 rounded-xl border px-3 pt-2 pb-2 shadow-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,12 @@ export const Reasoning = memo(
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</ReasoningContext.Provider>
|
</ReasoningContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
export type ReasoningTriggerProps = ComponentProps<
|
||||||
|
typeof CollapsibleTrigger
|
||||||
|
> & {
|
||||||
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -126,14 +128,19 @@ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReasoningTrigger = memo(
|
export const ReasoningTrigger = memo(
|
||||||
({ className, children, getThinkingMessage = defaultGetThinkingMessage, ...props }: ReasoningTriggerProps) => {
|
({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
getThinkingMessage = defaultGetThinkingMessage,
|
||||||
|
...props
|
||||||
|
}: ReasoningTriggerProps) => {
|
||||||
const { isStreaming, isOpen, duration } = useReasoning();
|
const { isStreaming, isOpen, duration } = useReasoning();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
|
"text-muted-foreground hover:text-foreground flex w-full items-center gap-2 text-sm transition-colors",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -144,14 +151,14 @@ export const ReasoningTrigger = memo(
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 transition-transform",
|
"size-4 transition-transform",
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
isOpen ? "rotate-180" : "rotate-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ReasoningContentProps = ComponentProps<
|
export type ReasoningContentProps = ComponentProps<
|
||||||
|
|
@ -165,14 +172,14 @@ export const ReasoningContent = memo(
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-4 text-sm",
|
"mt-4 text-sm",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Streamdown {...props}>{children}</Streamdown>
|
<Streamdown {...props}>{children}</Streamdown>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Reasoning.displayName = "Reasoning";
|
Reasoning.displayName = "Reasoning";
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ const ShimmerComponent = ({
|
||||||
spread = 2,
|
spread = 2,
|
||||||
}: TextShimmerProps) => {
|
}: TextShimmerProps) => {
|
||||||
const MotionComponent = motion.create(
|
const MotionComponent = motion.create(
|
||||||
Component as keyof JSX.IntrinsicElements
|
Component as keyof JSX.IntrinsicElements,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dynamicSpread = useMemo(
|
const dynamicSpread = useMemo(
|
||||||
() => (children?.length ?? 0) * spread,
|
() => (children?.length ?? 0) * spread,
|
||||||
[children, spread]
|
[children, spread],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -39,8 +39,8 @@ const ShimmerComponent = ({
|
||||||
animate={{ backgroundPosition: "0% center" }}
|
animate={{ backgroundPosition: "0% center" }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
|
||||||
"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
|
"[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
initial={{ backgroundPosition: "100% center" }}
|
initial={{ backgroundPosition: "100% center" }}
|
||||||
style={
|
style={
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export type SourcesProps = ComponentProps<"div">;
|
||||||
|
|
||||||
export const Sources = ({ className, ...props }: SourcesProps) => (
|
export const Sources = ({ className, ...props }: SourcesProps) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
className={cn("not-prose text-primary mb-4 text-xs", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -50,8 +50,8 @@ export const SourcesContent = ({
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-3 flex w-fit flex-col gap-2",
|
"mt-3 flex w-fit flex-col gap-2",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Icon } from "@radix-ui/react-select";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Children, type ComponentProps } from "react";
|
import { Children, type ComponentProps } from "react";
|
||||||
|
|
||||||
|
|
@ -60,16 +61,17 @@ export const Suggestion = ({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal",
|
"cursor-pointer rounded-full px-[20px] py-[15px] text-xs font-normal",
|
||||||
|
"border-none bg-[#F9F8FA] text-[#666666]",
|
||||||
|
"hover:bg-[#EAE9EB] hover:text-[#150033]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
size={size}
|
size={size}
|
||||||
type="button"
|
type="button"
|
||||||
variant={variant}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{Icon && <Icon className="size-4" />}
|
{/* {Icon && <Icon className="size-4" />} */}
|
||||||
{children || suggestion}
|
{children || suggestion}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export const TaskItemFile = ({
|
||||||
}: TaskItemFileProps) => (
|
}: TaskItemFileProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs",
|
"bg-secondary text-foreground inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -57,7 +57,7 @@ export const TaskTrigger = ({
|
||||||
}: TaskTriggerProps) => (
|
}: TaskTriggerProps) => (
|
||||||
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
<CollapsibleTrigger asChild className={cn("group", className)} {...props}>
|
||||||
{children ?? (
|
{children ?? (
|
||||||
<div className="flex w-full cursor-pointer items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground">
|
<div className="text-muted-foreground hover:text-foreground flex w-full cursor-pointer items-center gap-2 text-sm transition-colors">
|
||||||
<SearchIcon className="size-4" />
|
<SearchIcon className="size-4" />
|
||||||
<p className="text-sm">{title}</p>
|
<p className="text-sm">{title}</p>
|
||||||
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||||
|
|
@ -75,12 +75,12 @@ export const TaskContent = ({
|
||||||
}: TaskContentProps) => (
|
}: TaskContentProps) => (
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
<div className="border-muted mt-4 space-y-2 border-l-2 pl-4">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ type ToolbarProps = ComponentProps<typeof NodeToolbar>;
|
||||||
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
export const Toolbar = ({ className, ...props }: ToolbarProps) => (
|
||||||
<NodeToolbar
|
<NodeToolbar
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 rounded-sm border bg-background p-1.5",
|
"bg-background flex items-center gap-1 rounded-sm border p-1.5",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
position={Position.Bottom}
|
position={Position.Bottom}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ export const WebPreview = ({
|
||||||
<WebPreviewContext.Provider value={contextValue}>
|
<WebPreviewContext.Provider value={contextValue}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex size-full flex-col rounded-lg border bg-card",
|
"bg-card flex size-full flex-col rounded-lg border",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -107,7 +107,7 @@ export const WebPreviewNavigationButton = ({
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="h-8 w-8 p-0 hover:text-foreground"
|
className="hover:text-foreground h-8 w-8 p-0"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -209,21 +209,21 @@ export const WebPreviewConsole = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
className={cn("border-t bg-muted/50 font-mono text-sm", className)}
|
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
|
||||||
onOpenChange={setConsoleOpen}
|
onOpenChange={setConsoleOpen}
|
||||||
open={consoleOpen}
|
open={consoleOpen}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex w-full items-center justify-between p-4 text-left font-medium hover:bg-muted/50"
|
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
Console
|
Console
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-4 w-4 transition-transform duration-200",
|
"h-4 w-4 transition-transform duration-200",
|
||||||
consoleOpen && "rotate-180"
|
consoleOpen && "rotate-180",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -231,7 +231,7 @@ export const WebPreviewConsole = ({
|
||||||
<CollapsibleContent
|
<CollapsibleContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-4 pb-4",
|
"px-4 pb-4",
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||||
|
|
@ -244,7 +244,7 @@ export const WebPreviewConsole = ({
|
||||||
"text-xs",
|
"text-xs",
|
||||||
log.level === "error" && "text-destructive",
|
log.level === "error" && "text-destructive",
|
||||||
log.level === "warn" && "text-yellow-600",
|
log.level === "warn" && "text-yellow-600",
|
||||||
log.level === "log" && "text-foreground"
|
log.level === "log" && "text-foreground",
|
||||||
)}
|
)}
|
||||||
key={`${log.timestamp.getTime()}-${index}`}
|
key={`${log.timestamp.getTime()}-${index}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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) + "?mock=true"}
|
href={pathOfThread(caseStudy.threadId)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Card className="group/card relative h-64 overflow-hidden">
|
<Card className="group/card relative h-64 overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
|
@ -16,8 +16,8 @@ const alertVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Alert({
|
function Alert({
|
||||||
className,
|
className,
|
||||||
|
|
@ -31,7 +31,7 @@ function Alert({
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="alert-title"
|
data-slot="alert-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertDescription({
|
function AlertDescription({
|
||||||
|
|
@ -56,11 +56,11 @@ function AlertDescription({
|
||||||
data-slot="alert-description"
|
data-slot="alert-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import React, { memo } from "react"
|
import React, { memo } from "react";
|
||||||
|
|
||||||
interface AuroraTextProps {
|
interface AuroraTextProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
className?: string
|
className?: string;
|
||||||
colors?: string[]
|
colors?: string[];
|
||||||
speed?: number
|
speed?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuroraText = memo(
|
export const AuroraText = memo(
|
||||||
|
|
@ -15,6 +16,7 @@ export const AuroraText = memo(
|
||||||
className = "",
|
className = "",
|
||||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||||
speed = 1,
|
speed = 1,
|
||||||
|
style,
|
||||||
}: AuroraTextProps) => {
|
}: AuroraTextProps) => {
|
||||||
const gradientStyle = {
|
const gradientStyle = {
|
||||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||||
|
|
@ -23,10 +25,10 @@ export const AuroraText = memo(
|
||||||
WebkitBackgroundClip: "text",
|
WebkitBackgroundClip: "text",
|
||||||
WebkitTextFillColor: "transparent",
|
WebkitTextFillColor: "transparent",
|
||||||
animationDuration: `${10 / speed}s`,
|
animationDuration: `${10 / speed}s`,
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`relative inline-block ${className}`}>
|
<span className={`relative inline-block ${className}`} style={style}>
|
||||||
<span className="sr-only">{children}</span>
|
<span className="sr-only">{children}</span>
|
||||||
<span
|
<span
|
||||||
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
||||||
|
|
@ -36,8 +38,8 @@ export const AuroraText = memo(
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
AuroraText.displayName = "AuroraText"
|
AuroraText.displayName = "AuroraText";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Avatar({
|
function Avatar({
|
||||||
className,
|
className,
|
||||||
|
|
@ -14,11 +14,11 @@ function Avatar({
|
||||||
data-slot="avatar"
|
data-slot="avatar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarImage({
|
function AvatarImage({
|
||||||
|
|
@ -31,7 +31,7 @@ function AvatarImage({
|
||||||
className={cn("aspect-square size-full", className)}
|
className={cn("aspect-square size-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarFallback({
|
function AvatarFallback({
|
||||||
|
|
@ -43,11 +43,11 @@ function AvatarFallback({
|
||||||
data-slot="avatar-fallback"
|
data-slot="avatar-fallback"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
|
@ -22,8 +22,8 @@ const badgeVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
|
|
@ -32,7 +32,7 @@ function Badge({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span"> &
|
}: React.ComponentProps<"span"> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "span"
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|
@ -40,7 +40,7 @@ function Badge({
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
|
@ -14,11 +14,11 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
data-slot="breadcrumb-list"
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
|
@ -28,7 +28,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbLink({
|
function BreadcrumbLink({
|
||||||
|
|
@ -36,9 +36,9 @@ function BreadcrumbLink({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"a"> & {
|
}: React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|
@ -46,7 +46,7 @@ function BreadcrumbLink({
|
||||||
className={cn("hover:text-foreground transition-colors", className)}
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
|
@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
className={cn("text-foreground font-normal", className)}
|
className={cn("text-foreground font-normal", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
function BreadcrumbSeparator({
|
||||||
|
|
@ -77,7 +77,7 @@ function BreadcrumbSeparator({
|
||||||
>
|
>
|
||||||
{children ?? <ChevronRight />}
|
{children ?? <ChevronRight />}
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
function BreadcrumbEllipsis({
|
||||||
|
|
@ -95,7 +95,7 @@ function BreadcrumbEllipsis({
|
||||||
<MoreHorizontal className="size-4" />
|
<MoreHorizontal className="size-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -106,4 +106,4 @@ export {
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
const buttonGroupVariants = cva(
|
const buttonGroupVariants = cva(
|
||||||
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
||||||
|
|
@ -18,8 +18,8 @@ const buttonGroupVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
orientation: "horizontal",
|
orientation: "horizontal",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function ButtonGroup({
|
function ButtonGroup({
|
||||||
className,
|
className,
|
||||||
|
|
@ -34,7 +34,7 @@ function ButtonGroup({
|
||||||
className={cn(buttonGroupVariants({ orientation }), className)}
|
className={cn(buttonGroupVariants({ orientation }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupText({
|
function ButtonGroupText({
|
||||||
|
|
@ -42,19 +42,19 @@ function ButtonGroupText({
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGroupSeparator({
|
function ButtonGroupSeparator({
|
||||||
|
|
@ -68,11 +68,11 @@ function ButtonGroupSeparator({
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -80,4 +80,4 @@ export {
|
||||||
ButtonGroupSeparator,
|
ButtonGroupSeparator,
|
||||||
ButtonGroupText,
|
ButtonGroupText,
|
||||||
buttonGroupVariants,
|
buttonGroupVariants,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const buttonVariants = cva(
|
||||||
secondary:
|
secondary:
|
||||||
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost:
|
||||||
"cursor-pointer hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"cursor-pointer bg-transparent hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
|
link: "cursor-pointer text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-[20px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -20,12 +20,12 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("leading-none font-semibold", className)}
|
className={cn("leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("px-6", className)}
|
className={cn("px-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -89,4 +89,4 @@ export {
|
||||||
CardAction,
|
CardAction,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,45 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import useEmblaCarousel, {
|
import useEmblaCarousel, {
|
||||||
type UseEmblaCarouselType,
|
type UseEmblaCarouselType,
|
||||||
} from "embla-carousel-react"
|
} from "embla-carousel-react";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react"
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
type CarouselOptions = UseCarouselParameters[0]
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
type CarouselPlugin = UseCarouselParameters[1]
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
type CarouselProps = {
|
type CarouselProps = {
|
||||||
opts?: CarouselOptions
|
opts?: CarouselOptions;
|
||||||
plugins?: CarouselPlugin
|
plugins?: CarouselPlugin;
|
||||||
orientation?: "horizontal" | "vertical"
|
orientation?: "horizontal" | "vertical";
|
||||||
setApi?: (api: CarouselApi) => void
|
setApi?: (api: CarouselApi) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
type CarouselContextProps = {
|
type CarouselContextProps = {
|
||||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
scrollPrev: () => void
|
scrollPrev: () => void;
|
||||||
scrollNext: () => void
|
scrollNext: () => void;
|
||||||
canScrollPrev: boolean
|
canScrollPrev: boolean;
|
||||||
canScrollNext: boolean
|
canScrollNext: boolean;
|
||||||
} & CarouselProps
|
} & CarouselProps;
|
||||||
|
|
||||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
function useCarousel() {
|
function useCarousel() {
|
||||||
const context = React.useContext(CarouselContext)
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useCarousel must be used within a <Carousel />")
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Carousel({
|
function Carousel({
|
||||||
|
|
@ -56,53 +56,53 @@ function Carousel({
|
||||||
...opts,
|
...opts,
|
||||||
axis: orientation === "horizontal" ? "x" : "y",
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
},
|
},
|
||||||
plugins
|
plugins,
|
||||||
)
|
);
|
||||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
if (!api) return
|
if (!api) return;
|
||||||
setCanScrollPrev(api.canScrollPrev())
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
setCanScrollNext(api.canScrollNext())
|
setCanScrollNext(api.canScrollNext());
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const scrollPrev = React.useCallback(() => {
|
const scrollPrev = React.useCallback(() => {
|
||||||
api?.scrollPrev()
|
api?.scrollPrev();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const scrollNext = React.useCallback(() => {
|
const scrollNext = React.useCallback(() => {
|
||||||
api?.scrollNext()
|
api?.scrollNext();
|
||||||
}, [api])
|
}, [api]);
|
||||||
|
|
||||||
const handleKeyDown = React.useCallback(
|
const handleKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key === "ArrowLeft") {
|
if (event.key === "ArrowLeft") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollPrev()
|
scrollPrev();
|
||||||
} else if (event.key === "ArrowRight") {
|
} else if (event.key === "ArrowRight") {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
scrollNext()
|
scrollNext();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scrollPrev, scrollNext]
|
[scrollPrev, scrollNext],
|
||||||
)
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api || !setApi) return
|
if (!api || !setApi) return;
|
||||||
setApi(api)
|
setApi(api);
|
||||||
}, [api, setApi])
|
}, [api, setApi]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!api) return
|
if (!api) return;
|
||||||
onSelect(api)
|
onSelect(api);
|
||||||
api.on("reInit", onSelect)
|
api.on("reInit", onSelect);
|
||||||
api.on("select", onSelect)
|
api.on("select", onSelect);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
api?.off("select", onSelect)
|
api?.off("select", onSelect);
|
||||||
}
|
};
|
||||||
}, [api, onSelect])
|
}, [api, onSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CarouselContext.Provider
|
<CarouselContext.Provider
|
||||||
|
|
@ -129,11 +129,11 @@ function Carousel({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</CarouselContext.Provider>
|
</CarouselContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { carouselRef, orientation } = useCarousel()
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -145,16 +145,16 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex",
|
"flex",
|
||||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
const { orientation } = useCarousel()
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -164,11 +164,11 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 shrink-0 grow-0 basis-full",
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselPrevious({
|
function CarouselPrevious({
|
||||||
|
|
@ -177,7 +177,7 @@ function CarouselPrevious({
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -189,7 +189,7 @@ function CarouselPrevious({
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -left-12 -translate-y-1/2"
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
onClick={scrollPrev}
|
onClick={scrollPrev}
|
||||||
|
|
@ -198,7 +198,7 @@ function CarouselPrevious({
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">Previous slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CarouselNext({
|
function CarouselNext({
|
||||||
|
|
@ -207,7 +207,7 @@ function CarouselNext({
|
||||||
size = "icon",
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -219,7 +219,7 @@ function CarouselNext({
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "top-1/2 -right-12 -translate-y-1/2"
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
onClick={scrollNext}
|
onClick={scrollNext}
|
||||||
|
|
@ -228,7 +228,7 @@ function CarouselNext({
|
||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">Next slide</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -238,4 +238,4 @@ export {
|
||||||
CarouselItem,
|
CarouselItem,
|
||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
CarouselNext,
|
CarouselNext,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { SearchIcon } from "lucide-react"
|
import { SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
function Command({
|
function Command({
|
||||||
className,
|
className,
|
||||||
|
|
@ -22,11 +22,11 @@ function Command({
|
||||||
data-slot="command"
|
data-slot="command"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandDialog({
|
function CommandDialog({
|
||||||
|
|
@ -37,10 +37,10 @@ function CommandDialog({
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Dialog> & {
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
title?: string
|
title?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
|
|
@ -57,7 +57,7 @@ function CommandDialog({
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandInput({
|
function CommandInput({
|
||||||
|
|
@ -74,12 +74,12 @@ function CommandInput({
|
||||||
data-slot="command-input"
|
data-slot="command-input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandList({
|
function CommandList({
|
||||||
|
|
@ -91,11 +91,11 @@ function CommandList({
|
||||||
data-slot="command-list"
|
data-slot="command-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandEmpty({
|
function CommandEmpty({
|
||||||
|
|
@ -107,7 +107,7 @@ function CommandEmpty({
|
||||||
className="py-6 text-center text-sm"
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandGroup({
|
function CommandGroup({
|
||||||
|
|
@ -119,11 +119,11 @@ function CommandGroup({
|
||||||
data-slot="command-group"
|
data-slot="command-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandSeparator({
|
function CommandSeparator({
|
||||||
|
|
@ -136,7 +136,7 @@ function CommandSeparator({
|
||||||
className={cn("bg-border -mx-1 h-px", className)}
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandItem({
|
function CommandItem({
|
||||||
|
|
@ -148,11 +148,11 @@ function CommandItem({
|
||||||
data-slot="command-item"
|
data-slot="command-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommandShortcut({
|
function CommandShortcut({
|
||||||
|
|
@ -164,11 +164,11 @@ function CommandShortcut({
|
||||||
data-slot="command-shortcut"
|
data-slot="command-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -181,4 +181,4 @@ export {
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function DevDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dev-dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dev-dialog-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dev-dialog-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dev-dialog-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dev-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DevDialogPortal data-slot="dev-dialog-portal">
|
||||||
|
<DevDialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dev-dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-[400px] max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-[#ffffff] p-[40px] shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dev-dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DevDialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dev-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogFooter({
|
||||||
|
className,
|
||||||
|
singleColumn = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { singleColumn?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dev-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"grid w-full justify-between gap-[30px] sm:flex-row",
|
||||||
|
singleColumn ? "grid-cols-1" : "grid-cols-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dev-dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dev-dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DevDialog,
|
||||||
|
DevDialogClose,
|
||||||
|
DevDialogContent,
|
||||||
|
DevDialogDescription,
|
||||||
|
DevDialogFooter,
|
||||||
|
DevDialogHeader,
|
||||||
|
DevDialogOverlay,
|
||||||
|
DevDialogPortal,
|
||||||
|
DevDialogTitle,
|
||||||
|
DevDialogTrigger,
|
||||||
|
};
|
||||||
|
|
@ -1,33 +1,33 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
|
|
@ -39,11 +39,11 @@ function DialogOverlay({
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
|
|
@ -52,7 +52,7 @@ function DialogContent({
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
|
@ -61,7 +61,7 @@ function DialogContent({
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -77,7 +77,7 @@ function DialogContent({
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -96,11 +96,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
|
|
@ -113,7 +113,7 @@ function DialogTitle({
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
|
|
@ -126,7 +126,7 @@ function DialogDescription({
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -140,4 +140,4 @@ export {
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
function DropdownMenuPortal({
|
||||||
|
|
@ -17,18 +17,20 @@ function DropdownMenuPortal({
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
function DropdownMenuTrigger({
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Trigger
|
<DropdownMenuPrimitive.Trigger
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot="dropdown-menu-trigger"
|
||||||
|
className={cn(className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuContent({
|
function DropdownMenuContent({
|
||||||
|
|
@ -42,13 +44,13 @@ function DropdownMenuContent({
|
||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[20px] border p-[20px] shadow-md",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
function DropdownMenuGroup({
|
||||||
|
|
@ -56,7 +58,7 @@ function DropdownMenuGroup({
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuItem({
|
function DropdownMenuItem({
|
||||||
|
|
@ -65,8 +67,8 @@ function DropdownMenuItem({
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
variant?: "default" | "destructive"
|
variant?: "default" | "destructive";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
|
|
@ -75,11 +77,11 @@ function DropdownMenuItem({
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
function DropdownMenuCheckboxItem({
|
||||||
|
|
@ -93,7 +95,7 @@ function DropdownMenuCheckboxItem({
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -105,7 +107,7 @@ function DropdownMenuCheckboxItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
function DropdownMenuRadioGroup({
|
||||||
|
|
@ -116,7 +118,7 @@ function DropdownMenuRadioGroup({
|
||||||
data-slot="dropdown-menu-radio-group"
|
data-slot="dropdown-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
function DropdownMenuRadioItem({
|
||||||
|
|
@ -128,8 +130,8 @@ function DropdownMenuRadioItem({
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 whitespace-nowrap rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none overflow-hidden data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -140,7 +142,7 @@ function DropdownMenuRadioItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
function DropdownMenuLabel({
|
||||||
|
|
@ -148,7 +150,7 @@ function DropdownMenuLabel({
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
|
|
@ -156,11 +158,11 @@ function DropdownMenuLabel({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
function DropdownMenuSeparator({
|
||||||
|
|
@ -173,7 +175,7 @@ function DropdownMenuSeparator({
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
|
|
@ -185,17 +187,17 @@ function DropdownMenuShortcut({
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSub({
|
function DropdownMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
function DropdownMenuSubTrigger({
|
||||||
|
|
@ -204,7 +206,7 @@ function DropdownMenuSubTrigger({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
|
@ -212,14 +214,14 @@ function DropdownMenuSubTrigger({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
function DropdownMenuSubContent({
|
||||||
|
|
@ -230,12 +232,12 @@ function DropdownMenuSubContent({
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-[20px] border p-1 shadow-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -254,4 +256,4 @@ export {
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { cn, truncateMiddle } from "@/lib/utils";
|
||||||
|
|
||||||
|
export interface DropdownSelectorOption<T extends string> {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownSelectorProps<T extends string> {
|
||||||
|
value: T;
|
||||||
|
options: DropdownSelectorOption<T>[];
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
triggerClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChevronDownIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="6"
|
||||||
|
viewBox="0 0 10 6"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.75 0.75L4.75 4.75L8.75 0.75"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChevronUpIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="6"
|
||||||
|
viewBox="0 0 10 6"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.75 4.75L4.75 0.75L8.75 4.75"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DropdownSelector<T extends string>({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
triggerClassName,
|
||||||
|
contentClassName,
|
||||||
|
}: DropdownSelectorProps<T>) {
|
||||||
|
const selectedOption = options.find((opt) => opt.value === value);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
className={
|
||||||
|
triggerClassName ??
|
||||||
|
"border-none bg-transparent flex justify-center w-full overflow-hidden text-ellipsis whitespace-nowrap shadow-none select-none focus:outline-none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex w-full justify-center items-center gap-1">
|
||||||
|
{truncateMiddle(selectedOption?.label ?? value, 50)}
|
||||||
|
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className={cn(contentClassName, "max-w-80")}>
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
value={value}
|
||||||
|
onValueChange={(v) => onChange(v as T)}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
title={option.label}
|
||||||
|
>
|
||||||
|
{truncateMiddle(option.label)}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -8,11 +8,11 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="empty"
|
data-slot="empty"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -21,11 +21,11 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="empty-header"
|
data-slot="empty-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
"flex max-w-sm flex-col items-center gap-2 text-center",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
const emptyMediaVariants = cva(
|
||||||
|
|
@ -40,8 +40,8 @@ const emptyMediaVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function EmptyMedia({
|
function EmptyMedia({
|
||||||
className,
|
className,
|
||||||
|
|
@ -55,7 +55,7 @@ function EmptyMedia({
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
className={cn(emptyMediaVariants({ variant, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -65,7 +65,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("text-lg font-medium tracking-tight", className)}
|
className={cn("text-lg font-medium tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
|
@ -74,11 +74,11 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
data-slot="empty-description"
|
data-slot="empty-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -87,11 +87,11 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="empty-content"
|
data-slot="empty-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -101,4 +101,4 @@ export {
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -198,28 +198,11 @@ 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,14 +1,14 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function HoverCard({
|
function HoverCard({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoverCardTrigger({
|
function HoverCardTrigger({
|
||||||
|
|
@ -16,7 +16,7 @@ function HoverCardTrigger({
|
||||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoverCardContent({
|
function HoverCardContent({
|
||||||
|
|
@ -33,12 +33,12 @@ function HoverCardContent({
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</HoverCardPrimitive.Portal>
|
</HoverCardPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
|
|
|
||||||
|
|
@ -14,20 +14,20 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none",
|
"group/input-group dark:bg-background/80 relative flex w-full max-w-[720px] items-center overflow-hidden rounded-md bg-[#FBFAFC] transition-[color,box-shadow] outline-none",
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
"has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
||||||
|
|
||||||
// Focus state.
|
// Focus state.
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-input has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
"has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
@ -152,7 +152,7 @@ function InputGroupTextarea({
|
||||||
<Textarea
|
<Textarea
|
||||||
data-slot="input-group-control"
|
data-slot="input-group-control"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
"flex-1 resize-none rounded-none border-0 bg-transparent p-[20px] shadow-none focus-visible:ring-0 dark:bg-transparent",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -13,7 +13,7 @@ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("group/item-group flex flex-col", className)}
|
className={cn("group/item-group flex flex-col", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemSeparator({
|
function ItemSeparator({
|
||||||
|
|
@ -27,7 +27,7 @@ function ItemSeparator({
|
||||||
className={cn("my-0", className)}
|
className={cn("my-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemVariants = cva(
|
const itemVariants = cva(
|
||||||
|
|
@ -48,8 +48,8 @@ const itemVariants = cva(
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Item({
|
function Item({
|
||||||
className,
|
className,
|
||||||
|
|
@ -59,7 +59,7 @@ function Item({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> &
|
}: React.ComponentProps<"div"> &
|
||||||
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="item"
|
data-slot="item"
|
||||||
|
|
@ -68,7 +68,7 @@ function Item({
|
||||||
className={cn(itemVariants({ variant, size, className }))}
|
className={cn(itemVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemMediaVariants = cva(
|
const itemMediaVariants = cva(
|
||||||
|
|
@ -85,8 +85,8 @@ const itemMediaVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function ItemMedia({
|
function ItemMedia({
|
||||||
className,
|
className,
|
||||||
|
|
@ -100,7 +100,7 @@ function ItemMedia({
|
||||||
className={cn(itemMediaVariants({ variant, className }))}
|
className={cn(itemMediaVariants({ variant, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -109,11 +109,11 @@ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="item-content"
|
data-slot="item-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -122,11 +122,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="item-title"
|
data-slot="item-title"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
|
@ -136,11 +136,11 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
||||||
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -150,7 +150,7 @@ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex items-center gap-2", className)}
|
className={cn("flex items-center gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -159,11 +159,11 @@ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="item-header"
|
data-slot="item-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex basis-full items-center justify-between gap-2",
|
"flex basis-full items-center justify-between gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -172,11 +172,11 @@ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="item-footer"
|
data-slot="item-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex basis-full items-center justify-between gap-2",
|
"flex basis-full items-center justify-between gap-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -190,4 +190,4 @@ export {
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemHeader,
|
ItemHeader,
|
||||||
ItemFooter,
|
ItemFooter,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
|
|
||||||
/* Border glow effect */
|
/* Border glow effect */
|
||||||
.magic-bento-card--border-glow::after {
|
.magic-bento-card--border-glow::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.particle::before {
|
.particle::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: -2px;
|
||||||
left: -2px;
|
left: -2px;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Progress({
|
function Progress({
|
||||||
className,
|
className,
|
||||||
|
|
@ -15,7 +15,7 @@ function Progress({
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -25,7 +25,7 @@ function Progress({
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Progress }
|
export { Progress };
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ function ResizableHandle({
|
||||||
<ResizablePrimitive.Separator
|
<ResizablePrimitive.Separator
|
||||||
data-slot="resizable-handle"
|
data-slot="resizable-handle"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
"focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function ScrollArea({
|
function ScrollArea({
|
||||||
className,
|
className,
|
||||||
|
|
@ -25,7 +25,7 @@ function ScrollArea({
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScrollBar({
|
function ScrollBar({
|
||||||
|
|
@ -43,7 +43,7 @@ function ScrollBar({
|
||||||
"h-full w-2.5 border-l border-l-transparent",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
orientation === "horizontal" &&
|
orientation === "horizontal" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent",
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -52,7 +52,7 @@ function ScrollBar({
|
||||||
className="bg-border relative flex-1 rounded-full"
|
className="bg-border relative flex-1 rounded-full"
|
||||||
/>
|
/>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar };
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
|
|
@ -18,11 +18,11 @@ function Separator({
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Separator }
|
export { Separator };
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTrigger({
|
function SheetTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetClose({
|
function SheetClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetPortal({
|
function SheetPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetOverlay({
|
function SheetOverlay({
|
||||||
|
|
@ -37,11 +37,11 @@ function SheetOverlay({
|
||||||
data-slot="sheet-overlay"
|
data-slot="sheet-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetContent({
|
function SheetContent({
|
||||||
|
|
@ -50,7 +50,7 @@ function SheetContent({
|
||||||
side = "right",
|
side = "right",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
|
|
@ -67,7 +67,7 @@ function SheetContent({
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
side === "bottom" &&
|
side === "bottom" &&
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -78,7 +78,7 @@ function SheetContent({
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
|
@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetTitle({
|
function SheetTitle({
|
||||||
|
|
@ -111,7 +111,7 @@ function SheetTitle({
|
||||||
className={cn("text-foreground font-semibold", className)}
|
className={cn("text-foreground font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SheetDescription({
|
function SheetDescription({
|
||||||
|
|
@ -124,7 +124,7 @@ function SheetDescription({
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -136,4 +136,4 @@ export {
|
||||||
SheetFooter,
|
SheetFooter,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
/**
|
/**
|
||||||
* Width of the border in pixels
|
* Width of the border in pixels
|
||||||
* @default 1
|
* @default 1
|
||||||
*/
|
*/
|
||||||
borderWidth?: number
|
borderWidth?: number;
|
||||||
/**
|
/**
|
||||||
* Duration of the animation in seconds
|
* Duration of the animation in seconds
|
||||||
* @default 14
|
* @default 14
|
||||||
*/
|
*/
|
||||||
duration?: number
|
duration?: number;
|
||||||
/**
|
/**
|
||||||
* Color of the border, can be a single color or an array of colors
|
* Color of the border, can be a single color or an array of colors
|
||||||
* @default "#000000"
|
* @default "#000000"
|
||||||
*/
|
*/
|
||||||
shineColor?: string | string[]
|
shineColor?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,9 +55,9 @@ export function ShineBorder({
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
|
"motion-safe:animate-shine pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ function SidebarProvider({
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar m-auto flex min-h-svh w-full overflow-hidden rounded-t-[20px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -207,6 +207,7 @@ function Sidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
// !
|
||||||
className="group peer text-sidebar-foreground hidden md:block"
|
className="group peer text-sidebar-foreground hidden md:block"
|
||||||
data-state={state}
|
data-state={state}
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
|
|
@ -309,7 +310,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||||
<main
|
<main
|
||||||
data-slot="sidebar-inset"
|
data-slot="sidebar-inset"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background relative flex w-full flex-1 flex-col",
|
"relative flex w-full flex-1 flex-col",
|
||||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Skeleton }
|
export { Skeleton };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleCheckIcon,
|
CircleCheckIcon,
|
||||||
|
|
@ -6,23 +6,23 @@ import {
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
OctagonXIcon,
|
OctagonXIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: null,
|
||||||
info: <InfoIcon className="size-4" />,
|
info: null,
|
||||||
warning: <TriangleAlertIcon className="size-4" />,
|
warning: null,
|
||||||
error: <OctagonXIcon className="size-4" />,
|
error: null,
|
||||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
loading: null,
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
|
@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { Toaster }
|
export { Toaster };
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-spotlight::before {
|
.card-spotlight::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 80%);
|
background: radial-gradient(
|
||||||
|
circle at var(--mouse-x) var(--mouse-y),
|
||||||
|
var(--spotlight-color),
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Switch({
|
function Switch({
|
||||||
className,
|
className,
|
||||||
|
|
@ -14,18 +14,18 @@ function Switch({
|
||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SwitchPrimitive.Thumb
|
<SwitchPrimitive.Thumb
|
||||||
data-slot="switch-thumb"
|
data-slot="switch-thumb"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({
|
||||||
className,
|
className,
|
||||||
|
|
@ -18,11 +18,11 @@ function Tabs({
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsListVariants = cva(
|
const tabsListVariants = cva(
|
||||||
|
|
@ -37,8 +37,8 @@ const tabsListVariants = cva(
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({
|
||||||
className,
|
className,
|
||||||
|
|
@ -53,7 +53,7 @@ function TabsList({
|
||||||
className={cn(tabsListVariants({ variant }), className)}
|
className={cn(tabsListVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({
|
||||||
|
|
@ -68,11 +68,11 @@ function TabsTrigger({
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({
|
||||||
|
|
@ -85,7 +85,7 @@ function TabsContent({
|
||||||
className={cn("flex-1 outline-none", className)}
|
className={cn("flex-1 outline-none", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ function ToggleGroupItem({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
size: context.size || size,
|
||||||
}),
|
}),
|
||||||
"w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10",
|
"h-full w-[50px] min-w-0 shrink-0 cursor-pointer bg-white px-3 focus:z-10 focus-visible:z-10",
|
||||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
"data-[spacing=0]:data-[variant=outline] data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
|
|
@ -25,8 +25,8 @@ const toggleVariants = cva(
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Toggle({
|
function Toggle({
|
||||||
className,
|
className,
|
||||||
|
|
@ -41,7 +41,7 @@ function Toggle({
|
||||||
className={cn(toggleVariants({ variant, size, className }))}
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Toggle, toggleVariants }
|
export { Toggle, toggleVariants };
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ function TooltipContent({
|
||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset ?? 4}
|
sideOffset={sideOffset ?? 4}
|
||||||
className={cn(
|
className={cn(
|
||||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-foreground text-background dark:text-foreground z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md border px-3 py-1.5 text-xs text-balance shadow-xs dark:border-white/18 dark:bg-[#050504]",
|
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-tooltip-background text-background dark:text-foreground z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md border px-3 py-1.5 text-xs text-balance shadow-xs dark:border-white/18 dark:bg-[#050504]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BotIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { type Agent } from "@/core/agents";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export function AgentWelcome({
|
|
||||||
className,
|
|
||||||
agent,
|
|
||||||
agentName,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
agent: Agent | null | undefined;
|
|
||||||
agentName: string;
|
|
||||||
}) {
|
|
||||||
const displayName = agent?.name ?? agentName;
|
|
||||||
const description = agent?.description;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"mx-auto flex w-full flex-col items-center justify-center gap-2 px-8 py-4 text-center",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-full">
|
|
||||||
<BotIcon className="text-primary h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">{displayName}</div>
|
|
||||||
{description && (
|
|
||||||
<p className="text-muted-foreground max-w-sm text-sm">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BotIcon, MessageSquareIcon, Trash2Icon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { useDeleteAgent } from "@/core/agents";
|
|
||||||
import type { Agent } from "@/core/agents";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
|
|
||||||
interface AgentCardProps {
|
|
||||||
agent: Agent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AgentCard({ agent }: AgentCardProps) {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const deleteAgent = useDeleteAgent();
|
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
||||||
|
|
||||||
function handleChat() {
|
|
||||||
router.push(`/workspace/agents/${agent.name}/chats/new`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete() {
|
|
||||||
try {
|
|
||||||
await deleteAgent.mutateAsync(agent.name);
|
|
||||||
toast.success(t.agents.deleteSuccess);
|
|
||||||
setDeleteOpen(false);
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(err instanceof Error ? err.message : String(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className="group flex flex-col transition-shadow hover:shadow-md">
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
|
||||||
<BotIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<CardTitle className="truncate text-base">
|
|
||||||
{agent.name}
|
|
||||||
</CardTitle>
|
|
||||||
{agent.model && (
|
|
||||||
<Badge variant="secondary" className="mt-0.5 text-xs">
|
|
||||||
{agent.model}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{agent.description && (
|
|
||||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
|
||||||
{agent.description}
|
|
||||||
</CardDescription>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
{agent.tool_groups && agent.tool_groups.length > 0 && (
|
|
||||||
<CardContent className="pt-0 pb-3">
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{agent.tool_groups.map((group) => (
|
|
||||||
<Badge key={group} variant="outline" className="text-xs">
|
|
||||||
{group}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CardFooter className="mt-auto flex items-center justify-between gap-2 pt-3">
|
|
||||||
<Button size="sm" className="flex-1" onClick={handleChat}>
|
|
||||||
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
|
|
||||||
{t.agents.chat}
|
|
||||||
</Button>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
|
||||||
onClick={() => setDeleteOpen(true)}
|
|
||||||
title={t.agents.delete}
|
|
||||||
>
|
|
||||||
<Trash2Icon className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Delete Confirm */}
|
|
||||||
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t.agents.delete}</DialogTitle>
|
|
||||||
<DialogDescription>{t.agents.deleteConfirm}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setDeleteOpen(false)}
|
|
||||||
disabled={deleteAgent.isPending}
|
|
||||||
>
|
|
||||||
{t.common.cancel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleDelete}
|
|
||||||
disabled={deleteAgent.isPending}
|
|
||||||
>
|
|
||||||
{deleteAgent.isPending ? t.common.loading : t.common.delete}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { BotIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useAgents } from "@/core/agents";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
|
|
||||||
import { AgentCard } from "./agent-card";
|
|
||||||
|
|
||||||
export function AgentGallery() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { agents, isLoading } = useAgents();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const handleNewAgent = () => {
|
|
||||||
router.push("/workspace/agents/new");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex size-full flex-col">
|
|
||||||
{/* Page header */}
|
|
||||||
<div className="flex items-center justify-between border-b px-6 py-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-semibold">{t.agents.title}</h1>
|
|
||||||
<p className="text-muted-foreground mt-0.5 text-sm">
|
|
||||||
{t.agents.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleNewAgent}>
|
|
||||||
<PlusIcon className="mr-1.5 h-4 w-4" />
|
|
||||||
{t.agents.newAgent}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
|
|
||||||
{t.common.loading}
|
|
||||||
</div>
|
|
||||||
) : agents.length === 0 ? (
|
|
||||||
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
|
||||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
|
||||||
<BotIcon className="text-muted-foreground h-7 w-7" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{t.agents.emptyTitle}</p>
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
|
||||||
{t.agents.emptyDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" className="mt-2" onClick={handleNewAgent}>
|
|
||||||
<PlusIcon className="mr-1.5 h-4 w-4" />
|
|
||||||
{t.agents.newAgent}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
||||||
{agents.map((agent) => (
|
|
||||||
<AgentCard key={agent.name} agent={agent} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
|
import { DownloadIcon, FileTextIcon, LoaderIcon, FileTypeIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Code2Icon,
|
useCallback,
|
||||||
CopyIcon,
|
useEffect,
|
||||||
DownloadIcon,
|
useMemo,
|
||||||
EyeIcon,
|
useState,
|
||||||
LoaderIcon,
|
type HTMLAttributes,
|
||||||
PackageIcon,
|
} from "react";
|
||||||
SquareArrowOutUpRightIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
|
|
||||||
|
|
@ -20,27 +17,26 @@ import {
|
||||||
ArtifactHeader,
|
ArtifactHeader,
|
||||||
ArtifactTitle,
|
ArtifactTitle,
|
||||||
} from "@/components/ai-elements/artifact";
|
} from "@/components/ai-elements/artifact";
|
||||||
import { Select, SelectItem } from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
SelectContent,
|
DropdownMenu,
|
||||||
SelectGroup,
|
DropdownMenuContent,
|
||||||
SelectTrigger,
|
DropdownMenuItem,
|
||||||
SelectValue,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { DropdownSelector } from "@/components/ui/dropdown-selector";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
import { CodeEditor } from "@/components/workspace/code-editor";
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { installSkill } from "@/core/skills/api";
|
import { installSkill } from "@/core/skills/api";
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||||
import { env } from "@/env";
|
import { useMarkdownDownload } from "@/core/utils/markdown-download";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, copyToClipboard, truncateMiddle } from "@/lib/utils";
|
||||||
|
|
||||||
import { ArtifactLink } from "../citations/artifact-link";
|
import { CitationLink } from "../citations/citation-link";
|
||||||
import { useThread } from "../messages/context";
|
|
||||||
import { Tooltip } from "../tooltip";
|
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
import { useArtifacts } from "./context";
|
||||||
|
|
||||||
|
|
@ -54,7 +50,8 @@ export function ArtifactFileDetail({
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select } = useArtifacts();
|
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||||
|
useArtifacts();
|
||||||
const isWriteFile = useMemo(() => {
|
const isWriteFile = useMemo(() => {
|
||||||
return filepathFromProps.startsWith("write-file:");
|
return filepathFromProps.startsWith("write-file:");
|
||||||
}, [filepathFromProps]);
|
}, [filepathFromProps]);
|
||||||
|
|
@ -80,9 +77,9 @@ export function ArtifactFileDetail({
|
||||||
}
|
}
|
||||||
return checkCodeFile(filepath);
|
return checkCodeFile(filepath);
|
||||||
}, [filepath, isWriteFile, isSkillFile]);
|
}, [filepath, isWriteFile, isSkillFile]);
|
||||||
const isSupportPreview = useMemo(() => {
|
const previewable = useMemo(() => {
|
||||||
return language === "html" || language === "markdown";
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
||||||
}, [language]);
|
}, [isWriteFile, language]);
|
||||||
const { content } = useArtifactContent({
|
const { content } = useArtifactContent({
|
||||||
threadId,
|
threadId,
|
||||||
filepath: filepathFromProps,
|
filepath: filepathFromProps,
|
||||||
|
|
@ -91,16 +88,62 @@ export function ArtifactFileDetail({
|
||||||
|
|
||||||
const displayContent = content ?? "";
|
const displayContent = content ?? "";
|
||||||
|
|
||||||
|
const artifactOptions = useMemo(() => {
|
||||||
|
return (artifacts ?? []).map((artifactPath) => ({
|
||||||
|
value: artifactPath,
|
||||||
|
label: getFileName(artifactPath),
|
||||||
|
}));
|
||||||
|
}, [artifacts]);
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const { isMock } = useThread();
|
const [zoom, setZoom] = useState(80);
|
||||||
|
|
||||||
|
// 获取文件名(不含路径)
|
||||||
|
const fileName = useMemo(() => getFileName(filepath), [filepath]);
|
||||||
|
|
||||||
|
// 是否可以转换为docx/pdf(仅markdown文件支持)
|
||||||
|
const canConvertToDocxPdf = language === "markdown";
|
||||||
|
|
||||||
|
// 使用 Markdown 下载 hook
|
||||||
|
const { isDownloading, downloadAsDocx, downloadAsPdf } = useMarkdownDownload({
|
||||||
|
onError: (error, format) => {
|
||||||
|
console.error(`Failed to download as ${format}:`, error);
|
||||||
|
toast.error(`Failed to download as ${format.toUpperCase()}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载为 DOCX
|
||||||
|
const handleDownloadDocx = useCallback(() => {
|
||||||
|
if (content) {
|
||||||
|
void downloadAsDocx(content, fileName);
|
||||||
|
}
|
||||||
|
}, [content, fileName, downloadAsDocx]);
|
||||||
|
|
||||||
|
// 下载为 PDF
|
||||||
|
const handleDownloadPdf = useCallback(() => {
|
||||||
|
if (content) {
|
||||||
|
void downloadAsPdf(content, fileName);
|
||||||
|
}
|
||||||
|
}, [content, fileName, downloadAsPdf]);
|
||||||
|
|
||||||
|
// 全屏切换处理
|
||||||
|
const handleFullscreenToggle = useCallback(() => {
|
||||||
|
const newFullscreen = !fullscreen;
|
||||||
|
setFullscreen(newFullscreen);
|
||||||
|
sendToParent({
|
||||||
|
type: POST_MESSAGE_TYPES.FULLSCREEN,
|
||||||
|
fullscreen: newFullscreen,
|
||||||
|
});
|
||||||
|
}, [fullscreen, setFullscreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSupportPreview) {
|
if (previewable) {
|
||||||
setViewMode("preview");
|
setViewMode("preview");
|
||||||
} else {
|
} else {
|
||||||
setViewMode("code");
|
setViewMode("code");
|
||||||
}
|
}
|
||||||
}, [isSupportPreview]);
|
}, [previewable]);
|
||||||
|
|
||||||
const handleInstallSkill = useCallback(async () => {
|
const handleInstallSkill = useCallback(async () => {
|
||||||
if (isInstalling) return;
|
if (isInstalling) return;
|
||||||
|
|
@ -123,38 +166,18 @@ export function ArtifactFileDetail({
|
||||||
setIsInstalling(false);
|
setIsInstalling(false);
|
||||||
}
|
}
|
||||||
}, [threadId, filepath, isInstalling]);
|
}, [threadId, filepath, isInstalling]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Artifact className={cn(className)}>
|
// 给滚动遮挡头部定位relative
|
||||||
<ArtifactHeader className="px-2">
|
<Artifact className={cn("relative",className)}>
|
||||||
<div className="flex items-center gap-2">
|
<ArtifactHeader>
|
||||||
<ArtifactTitle>
|
<div className="flex items-center justify-start gap-2">
|
||||||
{isWriteFile ? (
|
{previewable && (
|
||||||
<div className="px-2">{getFileName(filepath)}</div>
|
|
||||||
) : (
|
|
||||||
<Select value={filepath} onValueChange={select}>
|
|
||||||
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
|
|
||||||
<SelectValue placeholder="Select a file" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="select-none">
|
|
||||||
<SelectGroup>
|
|
||||||
{(artifacts ?? []).map((filepath) => (
|
|
||||||
<SelectItem key={filepath} value={filepath}>
|
|
||||||
{getFileName(filepath)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</ArtifactTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-0 grow items-center justify-center">
|
|
||||||
{isSupportPreview && (
|
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
className="mx-auto"
|
|
||||||
type="single"
|
type="single"
|
||||||
variant="outline"
|
variant={null}
|
||||||
size="sm"
|
size="default"
|
||||||
|
className="h-[28px]"
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
@ -163,47 +186,75 @@ export function ArtifactFileDetail({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleGroupItem value="code">
|
<ToggleGroupItem value="code">
|
||||||
<Code2Icon />
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 6L2 9L5 12"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M11 3L7 15"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13 6L16 9L13 12"
|
||||||
|
stroke="#150033"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="preview">
|
<ToggleGroupItem value="preview">
|
||||||
<EyeIcon />
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 16 10"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 0.5C10.4943 0.5 12.8473 1.84466 14.792 4.21973C15.1644 4.67466 15.1644 5.32534 14.792 5.78027C12.8473 8.15534 10.4943 9.5 8 9.5C5.50561 9.49989 3.15269 8.15543 1.20801 5.78027C0.835561 5.32534 0.835562 4.67466 1.20801 4.21973C3.15269 1.84457 5.50561 0.500106 8 0.5Z"
|
||||||
|
stroke="#666666"
|
||||||
|
/>
|
||||||
|
<circle cx="8" cy="5" r="1.5" stroke="#666666" />
|
||||||
|
</svg>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
{/* 放大缩小选择器 */}
|
||||||
|
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-w-0 grow items-center justify-center">
|
||||||
|
<ArtifactTitle>
|
||||||
|
{isWriteFile ? (
|
||||||
|
<div className=" w-full text-center overflow-hidden text-ellipsis whitespace-nowrap px-2">{truncateMiddle(getFileName(filepath), 50)}</div>
|
||||||
|
) : (
|
||||||
|
<DropdownSelector
|
||||||
|
value={filepath}
|
||||||
|
options={artifactOptions}
|
||||||
|
onChange={select}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ArtifactTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end overflow-hidden">
|
||||||
<ArtifactActions>
|
<ArtifactActions>
|
||||||
{!isWriteFile && filepath.endsWith(".skill") && (
|
|
||||||
<Tooltip content={t.toolCalls.skillInstallTooltip}>
|
|
||||||
<ArtifactAction
|
|
||||||
icon={isInstalling ? LoaderIcon : PackageIcon}
|
|
||||||
label={t.common.install}
|
|
||||||
tooltip={t.common.install}
|
|
||||||
disabled={
|
|
||||||
isInstalling ||
|
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
|
|
||||||
}
|
|
||||||
onClick={handleInstallSkill}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{!isWriteFile && (
|
|
||||||
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
|
||||||
<ArtifactAction
|
|
||||||
icon={SquareArrowOutUpRightIcon}
|
|
||||||
label={t.common.openInNewWindow}
|
|
||||||
tooltip={t.common.openInNewWindow}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{isCodeFile && (
|
{isCodeFile && (
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
icon={CopyIcon}
|
|
||||||
label={t.clipboard.copyToClipboard}
|
label={t.clipboard.copyToClipboard}
|
||||||
disabled={!content}
|
disabled={!content}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(displayContent ?? "");
|
await copyToClipboard(displayContent ?? "");
|
||||||
toast.success(t.clipboard.copiedToClipboard);
|
toast.success(t.clipboard.copiedToClipboard);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to copy to clipboard");
|
toast.error("Failed to copy to clipboard");
|
||||||
|
|
@ -211,49 +262,210 @@ export function ArtifactFileDetail({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tooltip={t.clipboard.copyToClipboard}
|
tooltip={t.clipboard.copyToClipboard}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 2H13C14.1046 2 15 2.89543 15 4V13"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
|
<rect
|
||||||
|
x="2.5"
|
||||||
|
y="4.5"
|
||||||
|
width="10"
|
||||||
|
height="11"
|
||||||
|
rx="1.5"
|
||||||
|
stroke="#666666"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ArtifactAction>
|
||||||
)}
|
)}
|
||||||
{!isWriteFile && (
|
{!isWriteFile && (
|
||||||
<a
|
<DropdownMenu>
|
||||||
href={urlOfArtifact({ filepath, threadId, download: true })}
|
<DropdownMenuTrigger asChild>
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
icon={DownloadIcon}
|
|
||||||
label={t.common.download}
|
label={t.common.download}
|
||||||
tooltip={t.common.download}
|
tooltip={t.common.download}
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 9V14C16 15.1046 15.1046 16 14 16H4C2.89543 16 2 15.1046 2 14V9"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
</a>
|
<path
|
||||||
|
d="M9 2V13M9 13L5 9M9 13L13 9"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
</ArtifactAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<a
|
||||||
|
href={urlOfArtifact({
|
||||||
|
filepath,
|
||||||
|
threadId,
|
||||||
|
download: true,
|
||||||
|
})}
|
||||||
|
target="_blank"
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4" />
|
||||||
|
{t.common.downloadOriginal}
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{/* DOCX 和 PDF 导出选项仅对 Markdown 文件显示。 */}
|
||||||
|
{canConvertToDocxPdf && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleDownloadDocx}
|
||||||
|
disabled={isDownloading !== null || !content}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileTextIcon className="size-4" />
|
||||||
|
{isDownloading === "docx" ? t.common.loading : t.common.downloadAsDocx}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleDownloadPdf}
|
||||||
|
disabled={isDownloading !== null || !content}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileTypeIcon className="size-4" />
|
||||||
|
{isDownloading === "pdf" ? t.common.loading : t.common.downloadAsPdf}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
{/* 全屏按钮 */}
|
||||||
|
<ArtifactAction
|
||||||
|
label={
|
||||||
|
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||||
|
}
|
||||||
|
onClick={handleFullscreenToggle}
|
||||||
|
tooltip={
|
||||||
|
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{fullscreen ? (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 2V4C6 5.10457 5.10457 6 4 6H2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6 16V14C6 12.8954 5.10457 12 4 12H2"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 2V4C12 5.10457 12.8954 6 14 6H16"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 16V14C12 12.8954 12.8954 12 14 12H16"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.75 15.5H4.5C3.39543 15.5 2.5 14.6046 2.5 13.5V12.25M2.5 5.75V4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H5.75M12.25 2.5H13.5C14.6046 2.5 15.5 3.39543 15.5 4.5V5.75M15.5 12.25V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H12.25"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</ArtifactAction>
|
||||||
|
{!fullscreen && (
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
icon={XIcon}
|
|
||||||
label={t.common.close}
|
label={t.common.close}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
tooltip={t.common.close}
|
tooltip={t.common.close}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 14L14 4M4 4L14 14"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
</ArtifactAction>
|
||||||
|
)}
|
||||||
</ArtifactActions>
|
</ArtifactActions>
|
||||||
</div>
|
</div>
|
||||||
</ArtifactHeader>
|
</ArtifactHeader>
|
||||||
<ArtifactContent className="p-0">
|
<ArtifactContent className=" rounded-b-[10px] bg-white p-0">
|
||||||
{isSupportPreview &&
|
{/* 遮挡多余的滚动顶部 */}
|
||||||
|
<div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div>
|
||||||
|
{previewable &&
|
||||||
viewMode === "preview" &&
|
viewMode === "preview" &&
|
||||||
(language === "markdown" || language === "html") && (
|
(language === "markdown" || language === "html") && (
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={displayContent}
|
content={displayContent}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
|
zoom={zoom}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isCodeFile && viewMode === "code" && (
|
{isCodeFile && viewMode === "code" && (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none"
|
className="size-full py-[20px] resize-none rounded-none border-none"
|
||||||
value={displayContent ?? ""}
|
value={displayContent ?? ""}
|
||||||
|
zoom={zoom}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isCodeFile && (
|
{!isCodeFile && (
|
||||||
<iframe
|
<iframe
|
||||||
className="size-full"
|
className="size-full"
|
||||||
src={urlOfArtifact({ filepath, threadId, isMock })}
|
src={urlOfArtifact({ filepath, threadId })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ArtifactContent>
|
</ArtifactContent>
|
||||||
|
|
@ -264,17 +476,24 @@ export function ArtifactFileDetail({
|
||||||
export function ArtifactFilePreview({
|
export function ArtifactFilePreview({
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
|
zoom = 100,
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
zoom?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const zoomScale = zoom / 100;
|
||||||
|
|
||||||
if (language === "markdown") {
|
if (language === "markdown") {
|
||||||
return (
|
return (
|
||||||
<div className="size-full px-4">
|
<div
|
||||||
|
className={cn("size-full p-[20px]")}
|
||||||
|
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
className="size-full"
|
className="size-full"
|
||||||
{...streamdownPlugins}
|
{...streamdownPlugins}
|
||||||
components={{ a: ArtifactLink }}
|
components={{ a: CitationLink }}
|
||||||
>
|
>
|
||||||
{content ?? ""}
|
{content ?? ""}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
|
|
@ -288,8 +507,130 @@ export function ArtifactFilePreview({
|
||||||
title="Artifact preview"
|
title="Artifact preview"
|
||||||
srcDoc={content}
|
srcDoc={content}
|
||||||
sandbox="allow-scripts allow-forms"
|
sandbox="allow-scripts allow-forms"
|
||||||
|
style={{ zoom: zoomScale }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 缩放比例选项
|
||||||
|
const ZOOM_LEVELS = [50, 60, 70, 80, 90, 100, 110, 120, 130, 150, 175, 200];
|
||||||
|
|
||||||
|
export type ArtifactZoomSelectorProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
"onChange"
|
||||||
|
> & {
|
||||||
|
value?: number;
|
||||||
|
onChange?: (value: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArtifactZoomSelector = ({
|
||||||
|
value = 100,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ArtifactZoomSelectorProps) => {
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||||
|
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
||||||
|
if (currentIndex < ZOOM_LEVELS.length - 1 && nextValue !== undefined) {
|
||||||
|
onChange?.(nextValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||||
|
const prevValue = ZOOM_LEVELS[currentIndex - 1];
|
||||||
|
if (currentIndex > 0 && prevValue !== undefined) {
|
||||||
|
onChange?.(prevValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canZoomIn = ZOOM_LEVELS.indexOf(value) < ZOOM_LEVELS.length - 1;
|
||||||
|
const canZoomOut = ZOOM_LEVELS.indexOf(value) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-[28px] items-center gap-1 rounded-[10px] bg-white backdrop-blur-sm",
|
||||||
|
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
disabled={!canZoomIn}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-10 items-center justify-center rounded py-1 transition-colors",
|
||||||
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
|
)}
|
||||||
|
aria-label="放大"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||||
|
<path
|
||||||
|
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||||
|
fill="#666666"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.33325 7.5H9.7777M7.55547 5V10"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"min-w-[36px] text-center text-xs font-medium text-gray-600",
|
||||||
|
"dark:text-gray-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
disabled={!canZoomOut}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-10 items-center justify-center rounded transition-colors",
|
||||||
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
|
)}
|
||||||
|
aria-label="缩小"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666" />
|
||||||
|
<path
|
||||||
|
d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z"
|
||||||
|
fill="#666666"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.99927 7.5H9.99927"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
getFileIcon,
|
getFileIcon,
|
||||||
getFileName,
|
getFileName,
|
||||||
} from "@/core/utils/files";
|
} from "@/core/utils/files";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, truncateMiddle } from "@/lib/utils";
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
import { useArtifacts } from "./context";
|
||||||
|
|
||||||
|
|
@ -80,12 +80,14 @@ export function ArtifactFileList({
|
||||||
onClick={() => handleClick(file)}
|
onClick={() => handleClick(file)}
|
||||||
>
|
>
|
||||||
<CardHeader className="pr-2 pl-1">
|
<CardHeader className="pr-2 pl-1">
|
||||||
<CardTitle className="relative pl-8">
|
<CardTitle className=" relative pl-8 overflow-hidden">
|
||||||
<div>{getFileName(file)}</div>
|
<div className=" text-ellipsis whitespace-nowrap text-sm font-normal" title={getFileName(file)}>
|
||||||
<div className="absolute top-2 -left-0.5">
|
{truncateMiddle(getFileName(file), 50)}
|
||||||
{getFileIcon(file, "size-6")}
|
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<div className="absolute top-5 left-3">
|
||||||
|
{getFileIcon(file, "size-6 stroke-[1.5px] stroke-[#333333]")}
|
||||||
|
</div>
|
||||||
<CardDescription className="pl-8 text-xs">
|
<CardDescription className="pl-8 text-xs">
|
||||||
{getFileExtensionDisplayName(file)} file
|
{getFileExtensionDisplayName(file)} file
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { FilesIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Tooltip } from "@/components/workspace/tooltip";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
|
|
||||||
import { useArtifacts } from "./context";
|
|
||||||
|
|
||||||
export const ArtifactTrigger = () => {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { artifacts, setOpen: setArtifactsOpen } = useArtifacts();
|
|
||||||
|
|
||||||
if (!artifacts || artifacts.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tooltip content="Show artifacts of this conversation">
|
|
||||||
<Button
|
|
||||||
className="text-muted-foreground hover:text-foreground"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setArtifactsOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FilesIcon />
|
|
||||||
{t.common.artifacts}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { createContext, useContext, useState, type ReactNode } from "react";
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
|
|
@ -21,6 +15,9 @@ export interface ArtifactsContextType {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
autoOpen: boolean;
|
autoOpen: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
|
|
||||||
|
fullscreen: boolean;
|
||||||
|
setFullscreen: (fullscreen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
||||||
|
|
@ -39,10 +36,10 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
|
||||||
);
|
);
|
||||||
const [autoOpen, setAutoOpen] = useState(true);
|
const [autoOpen, setAutoOpen] = useState(true);
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
const { setOpen: setSidebarOpen } = useSidebar();
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
|
||||||
const select = useCallback(
|
const select = (artifact: string, autoSelect = false) => {
|
||||||
(artifact: string, autoSelect = false) => {
|
|
||||||
setSelectedArtifact(artifact);
|
setSelectedArtifact(artifact);
|
||||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
|
|
@ -50,15 +47,12 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
if (!autoSelect) {
|
if (!autoSelect) {
|
||||||
setAutoSelect(false);
|
setAutoSelect(false);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[setSidebarOpen, setSelectedArtifact, setAutoSelect],
|
|
||||||
);
|
|
||||||
|
|
||||||
const deselect = useCallback(() => {
|
const deselect = () => {
|
||||||
setSelectedArtifact(null);
|
setSelectedArtifact(null);
|
||||||
setAutoSelect(true);
|
setAutoSelect(true);
|
||||||
setOpen(false);
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
const value: ArtifactsContextType = {
|
const value: ArtifactsContextType = {
|
||||||
artifacts,
|
artifacts,
|
||||||
|
|
@ -78,6 +72,9 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
selectedArtifact,
|
selectedArtifact,
|
||||||
select,
|
select,
|
||||||
deselect,
|
deselect,
|
||||||
|
|
||||||
|
fullscreen,
|
||||||
|
setFullscreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
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,180 +0,0 @@
|
||||||
import { FilesIcon, XIcon } from "lucide-react";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import type { GroupImperativeHandle } from "react-resizable-panels";
|
|
||||||
|
|
||||||
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from "@/components/ui/resizable";
|
|
||||||
import { env } from "@/env";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ArtifactFileDetail,
|
|
||||||
ArtifactFileList,
|
|
||||||
useArtifacts,
|
|
||||||
} from "../artifacts";
|
|
||||||
import { useThread } from "../messages/context";
|
|
||||||
|
|
||||||
const CLOSE_MODE = { chat: 100, artifacts: 0 };
|
|
||||||
const OPEN_MODE = { chat: 60, artifacts: 40 };
|
|
||||||
|
|
||||||
const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
|
|
||||||
children,
|
|
||||||
threadId,
|
|
||||||
}) => {
|
|
||||||
const { thread } = useThread();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const threadIdRef = useRef(threadId);
|
|
||||||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
artifacts,
|
|
||||||
open: artifactsOpen,
|
|
||||||
setOpen: setArtifactsOpen,
|
|
||||||
setArtifacts,
|
|
||||||
select: selectArtifact,
|
|
||||||
deselect,
|
|
||||||
selectedArtifact,
|
|
||||||
} = useArtifacts();
|
|
||||||
|
|
||||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
if (threadIdRef.current !== threadId) {
|
|
||||||
threadIdRef.current = threadId;
|
|
||||||
deselect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update artifacts from the current thread
|
|
||||||
setArtifacts(thread.values.artifacts);
|
|
||||||
|
|
||||||
// DO NOT automatically deselect the artifact when switching threads, because the artifacts auto discovering is not work now.
|
|
||||||
// if (
|
|
||||||
// selectedArtifact &&
|
|
||||||
// !thread.values.artifacts?.includes(selectedArtifact)
|
|
||||||
// ) {
|
|
||||||
// deselect();
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (
|
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
|
||||||
autoSelectFirstArtifact
|
|
||||||
) {
|
|
||||||
if (thread?.values?.artifacts?.length > 0) {
|
|
||||||
setAutoSelectFirstArtifact(false);
|
|
||||||
selectArtifact(thread.values.artifacts[0]!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
threadId,
|
|
||||||
autoSelectFirstArtifact,
|
|
||||||
deselect,
|
|
||||||
selectArtifact,
|
|
||||||
selectedArtifact,
|
|
||||||
setArtifacts,
|
|
||||||
thread.values.artifacts,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const artifactPanelOpen = useMemo(() => {
|
|
||||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") {
|
|
||||||
return artifactsOpen && artifacts?.length > 0;
|
|
||||||
}
|
|
||||||
return artifactsOpen;
|
|
||||||
}, [artifactsOpen, artifacts]);
|
|
||||||
|
|
||||||
const resizableIdBase = useMemo(() => {
|
|
||||||
return pathname.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (layoutRef.current) {
|
|
||||||
if (artifactPanelOpen) {
|
|
||||||
layoutRef.current.setLayout(OPEN_MODE);
|
|
||||||
} else {
|
|
||||||
layoutRef.current.setLayout(CLOSE_MODE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [artifactPanelOpen]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResizablePanelGroup
|
|
||||||
id={`${resizableIdBase}-panels`}
|
|
||||||
orientation="horizontal"
|
|
||||||
defaultLayout={{ chat: 100, artifacts: 0 }}
|
|
||||||
groupRef={layoutRef}
|
|
||||||
>
|
|
||||||
<ResizablePanel className="relative" defaultSize={100} id="chat">
|
|
||||||
{children}
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle
|
|
||||||
id={`${resizableIdBase}-separator`}
|
|
||||||
className={cn(
|
|
||||||
"opacity-33 hover:opacity-100",
|
|
||||||
!artifactPanelOpen && "pointer-events-none opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ResizablePanel
|
|
||||||
className={cn(
|
|
||||||
"transition-all duration-300 ease-in-out",
|
|
||||||
!artifactsOpen && "opacity-0",
|
|
||||||
)}
|
|
||||||
id="artifacts"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full p-4 transition-transform duration-300 ease-in-out",
|
|
||||||
artifactPanelOpen ? "translate-x-0" : "translate-x-full",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{selectedArtifact ? (
|
|
||||||
<ArtifactFileDetail
|
|
||||||
className="size-full"
|
|
||||||
filepath={selectedArtifact}
|
|
||||||
threadId={threadId}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="relative flex size-full justify-center">
|
|
||||||
<div className="absolute top-1 right-1 z-30">
|
|
||||||
<Button
|
|
||||||
size="icon-sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setArtifactsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{thread.values.artifacts?.length === 0 ? (
|
|
||||||
<ConversationEmptyState
|
|
||||||
icon={<FilesIcon />}
|
|
||||||
title="No artifact selected"
|
|
||||||
description="Select an artifact to view its details"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
|
||||||
<header className="shrink-0">
|
|
||||||
<h2 className="text-lg font-medium">Artifacts</h2>
|
|
||||||
</header>
|
|
||||||
<main className="min-h-0 grow">
|
|
||||||
<ArtifactFileList
|
|
||||||
className="max-w-(--container-width-sm) p-4 pt-12"
|
|
||||||
files={thread.values.artifacts ?? []}
|
|
||||||
threadId={threadId}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
</ResizablePanelGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ChatBox };
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from "./chat-box";
|
|
||||||
export * from "./use-chat-mode";
|
|
||||||
export * from "./use-thread-chat";
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { uuid } from "@/core/utils/uuid";
|
|
||||||
|
|
||||||
export function useThreadChat() {
|
|
||||||
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const [threadId, setThreadId] = useState(() => {
|
|
||||||
return threadIdFromPath === "new" ? uuid() : threadIdFromPath;
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isNewThread, setIsNewThread] = useState(
|
|
||||||
() => threadIdFromPath === "new",
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (pathname.endsWith("/new")) {
|
|
||||||
setIsNewThread(true);
|
|
||||||
setThreadId(uuid());
|
|
||||||
}
|
|
||||||
}, [pathname]);
|
|
||||||
const isMock = searchParams.get("mock") === "true";
|
|
||||||
return { threadId, isNewThread, setIsNewThread, isMock };
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import type { AnchorHTMLAttributes } from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
import { CitationLink } from "./citation-link";
|
|
||||||
|
|
||||||
function isExternalUrl(href: string | undefined): boolean {
|
|
||||||
return !!href && /^https?:\/\//.test(href);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Link renderer for artifact markdown: citation: prefix → CitationLink, otherwise underlined text. */
|
|
||||||
export function ArtifactLink(props: AnchorHTMLAttributes<HTMLAnchorElement>) {
|
|
||||||
if (typeof props.children === "string") {
|
|
||||||
const match = /^citation:(.+)$/.exec(props.children);
|
|
||||||
if (match) {
|
|
||||||
const [, text] = match;
|
|
||||||
return <CitationLink {...props}>{text}</CitationLink>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -48,12 +48,12 @@ export function CitationLink({
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{displayText && (
|
{displayText && (
|
||||||
<h4 className="truncate font-medium text-sm leading-tight">
|
<h4 className="truncate text-sm leading-tight font-medium">
|
||||||
{displayText}
|
{displayText}
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
{href && (
|
{href && (
|
||||||
<p className="truncate break-all text-muted-foreground text-xs">
|
<p className="text-muted-foreground truncate text-xs break-all">
|
||||||
{href}
|
{href}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export function CodeEditor({
|
||||||
disabled,
|
disabled,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
settings,
|
settings,
|
||||||
|
zoom = 100,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
@ -50,6 +51,7 @@ export function CodeEditor({
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
settings?: unknown;
|
settings?: unknown;
|
||||||
|
zoom?: number;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
thread: { isLoading },
|
thread: { isLoading },
|
||||||
|
|
@ -70,12 +72,14 @@ export function CodeEditor({
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const zoomScale = (zoom ?? 100) / 100;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-text flex-col overflow-hidden rounded-md",
|
"flex cursor-text flex-col overflow-hidden rounded-md",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
KeyboardIcon,
|
|
||||||
MessageSquarePlusIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandShortcut,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import { useGlobalShortcuts } from "@/hooks/use-global-shortcuts";
|
|
||||||
|
|
||||||
import { SettingsDialog } from "./settings";
|
|
||||||
|
|
||||||
export function CommandPalette() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const router = useRouter();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleNewChat = useCallback(() => {
|
|
||||||
router.push("/workspace/chats/new");
|
|
||||||
setOpen(false);
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const handleOpenSettings = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
setSettingsOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleShowShortcuts = useCallback(() => {
|
|
||||||
setOpen(false);
|
|
||||||
setShortcutsOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const shortcuts = useMemo(
|
|
||||||
() => [
|
|
||||||
{ key: "k", meta: true, action: () => setOpen((o) => !o) },
|
|
||||||
{ key: "n", meta: true, shift: true, action: handleNewChat },
|
|
||||||
{ key: ",", meta: true, action: handleOpenSettings },
|
|
||||||
{ key: "/", meta: true, action: handleShowShortcuts },
|
|
||||||
],
|
|
||||||
[handleNewChat, handleOpenSettings, handleShowShortcuts],
|
|
||||||
);
|
|
||||||
|
|
||||||
useGlobalShortcuts(shortcuts);
|
|
||||||
|
|
||||||
|
|
||||||
const isMac =
|
|
||||||
typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
|
|
||||||
const metaKey = isMac ? "⌘" : "Ctrl+";
|
|
||||||
const shiftKey = isMac ? "⇧" : "Shift+";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<CommandInput placeholder={t.shortcuts.searchActions} />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>{t.shortcuts.noResults}</CommandEmpty>
|
|
||||||
<CommandGroup heading={t.shortcuts.actions}>
|
|
||||||
<CommandItem onSelect={handleNewChat}>
|
|
||||||
<MessageSquarePlusIcon className="mr-2 h-4 w-4" />
|
|
||||||
{t.sidebar.newChat}
|
|
||||||
<CommandShortcut>{metaKey}{shiftKey}N</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
<CommandItem onSelect={handleOpenSettings}>
|
|
||||||
<SettingsIcon className="mr-2 h-4 w-4" />
|
|
||||||
{t.common.settings}
|
|
||||||
<CommandShortcut>{metaKey},</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
<CommandItem onSelect={handleShowShortcuts}>
|
|
||||||
<KeyboardIcon className="mr-2 h-4 w-4" />
|
|
||||||
{t.shortcuts.keyboardShortcuts}
|
|
||||||
<CommandShortcut>{metaKey}/</CommandShortcut>
|
|
||||||
</CommandItem>
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
|
|
||||||
<Dialog open={shortcutsOpen} onOpenChange={setShortcutsOpen}>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t.shortcuts.keyboardShortcuts}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t.shortcuts.keyboardShortcutsDescription}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-3 text-sm">
|
|
||||||
{[
|
|
||||||
{ keys: `${metaKey}K`, label: t.shortcuts.openCommandPalette },
|
|
||||||
{ keys: `${metaKey}${shiftKey}N`, label: t.sidebar.newChat },
|
|
||||||
{ keys: `${metaKey}B`, label: t.shortcuts.toggleSidebar },
|
|
||||||
{ keys: `${metaKey},`, label: t.common.settings },
|
|
||||||
{
|
|
||||||
keys: `${metaKey}/`,
|
|
||||||
label: t.shortcuts.keyboardShortcuts,
|
|
||||||
},
|
|
||||||
].map(({ keys, label }) => (
|
|
||||||
<div key={keys} className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground">{label}</span>
|
|
||||||
<kbd className="bg-muted text-muted-foreground rounded px-2 py-0.5 font-mono text-xs">
|
|
||||||
{keys}
|
|
||||||
</kbd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useCallback, useState, type ComponentProps } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { copyToClipboard } from "@/lib/utils";
|
||||||
|
|
||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ export function CopyButton({
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
void navigator.clipboard.writeText(clipboardData);
|
void copyToClipboard(clipboardData);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
}, [clipboardData]);
|
}, [clipboardData]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Todo } from "@/core/todos";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
QueueItem,
|
||||||
|
QueueItemContent,
|
||||||
|
QueueItemIndicator,
|
||||||
|
QueueList,
|
||||||
|
} from "../ai-elements/queue";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
|
||||||
|
export function DevTodoList({
|
||||||
|
className,
|
||||||
|
todos,
|
||||||
|
trigger,
|
||||||
|
hidden,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
todos: Todo[];
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
hidden: boolean;
|
||||||
|
}) {
|
||||||
|
if (hidden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(todos);
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className={cn(
|
||||||
|
"z-[100] rounded-[20px] bg-white p-5 shadow-[0_0_20px_0_rgba(0,0,0,0.20)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<QueueList className="w-64">
|
||||||
|
{todos.map((todo, i) => (
|
||||||
|
<QueueItem key={i + (todo.content ?? "")}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<QueueItemIndicator
|
||||||
|
className={
|
||||||
|
todo.status === "in_progress" ? "bg-primary/70" : ""
|
||||||
|
}
|
||||||
|
completed={todo.status === "completed"}
|
||||||
|
/>
|
||||||
|
<QueueItemContent
|
||||||
|
className={
|
||||||
|
todo.status === "in_progress" ? "text-primary/70" : ""
|
||||||
|
}
|
||||||
|
completed={todo.status === "completed"}
|
||||||
|
>
|
||||||
|
{todo.content}
|
||||||
|
</QueueItemContent>
|
||||||
|
</div>
|
||||||
|
</QueueItem>
|
||||||
|
))}
|
||||||
|
</QueueList>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Download, FileJson, FileText } from "lucide-react";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import {
|
|
||||||
exportThreadAsJSON,
|
|
||||||
exportThreadAsMarkdown,
|
|
||||||
} from "@/core/threads/export";
|
|
||||||
import type { AgentThread } from "@/core/threads/types";
|
|
||||||
|
|
||||||
import { useThread } from "./messages/context";
|
|
||||||
import { Tooltip } from "./tooltip";
|
|
||||||
|
|
||||||
export function ExportTrigger({ threadId }: { threadId: string }) {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const { thread } = useThread();
|
|
||||||
|
|
||||||
const messages = thread.messages;
|
|
||||||
|
|
||||||
const handleExport = useCallback(
|
|
||||||
(format: "markdown" | "json") => {
|
|
||||||
if (messages.length === 0) {
|
|
||||||
toast.error(t.conversation.noMessages);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const agentThread = {
|
|
||||||
thread_id: threadId,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
values: thread.values,
|
|
||||||
} as AgentThread;
|
|
||||||
|
|
||||||
if (format === "markdown") {
|
|
||||||
exportThreadAsMarkdown(agentThread, messages);
|
|
||||||
} else {
|
|
||||||
exportThreadAsJSON(agentThread, messages);
|
|
||||||
}
|
|
||||||
toast.success(t.common.exportSuccess);
|
|
||||||
},
|
|
||||||
[messages, thread.values, threadId, t],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (messages.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<Tooltip content={t.common.export}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className="text-muted-foreground hover:text-foreground"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<Download />
|
|
||||||
{t.common.export}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onSelect={() => handleExport("markdown")}>
|
|
||||||
<FileText className="text-muted-foreground" />
|
|
||||||
<span>{t.common.exportAsMarkdown}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onSelect={() => handleExport("json")}>
|
|
||||||
<FileJson className="text-muted-foreground" />
|
|
||||||
<span>{t.common.exportAsJSON}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
||||||
|
import { copyToClipboard } from "@/lib/utils";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IframeTestPanel —— 仅用于开发阶段测试 iframe 通信功能
|
||||||
|
*
|
||||||
|
* 测试场景:
|
||||||
|
* 1. mode=skill 侧边栏隐藏
|
||||||
|
* 2. useSpecificChatMode 注入提示词
|
||||||
|
* 3. sendSelectSkill / openSkillDialog / clearSkill
|
||||||
|
*/
|
||||||
|
export function IframeTestPanel() {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const iframeSkill = useIframeSkill();
|
||||||
|
const [log, setLog] = useState<string[]>([]);
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const isSkillMode = searchParams.get("mode") === "skill";
|
||||||
|
|
||||||
|
function addLog(msg: string) {
|
||||||
|
setLog((prev) => [
|
||||||
|
`[${new Date().toLocaleTimeString()}] ${msg}`,
|
||||||
|
...prev.slice(0, 9),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnterSkillMode() {
|
||||||
|
router.push(`?mode=skill&skill_id=123&title=测试技能`);
|
||||||
|
addLog("进入 mode=skill,URL 已更新");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExitSkillMode() {
|
||||||
|
router.push(`?`);
|
||||||
|
addLog("退出 skill 模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSendSelectSkill() {
|
||||||
|
iframeSkill.sendSelectSkill("skill_001");
|
||||||
|
addLog("postMessage → selectSkill (skill_id=skill_001)");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenSkillDialog() {
|
||||||
|
iframeSkill.openSkillDialog();
|
||||||
|
addLog("postMessage → openSkillDialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearSkill() {
|
||||||
|
iframeSkill.clearSkill();
|
||||||
|
addLog("clearSkill 已调用,postMessage → skill_id=0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTestClipboardCopy() {
|
||||||
|
const testText = "测试复制内容 - " + new Date().toISOString();
|
||||||
|
copyToClipboard(testText);
|
||||||
|
addLog(`copyToClipboard → "${testText.slice(0, 30)}..."`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测是否在 iframe 中
|
||||||
|
const isInIframe = typeof window !== "undefined" && window.self !== window.top;
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="fixed bottom-24 left-3 z-[9999] rounded-full bg-violet-500 px-3 py-1 text-xs font-bold text-white shadow-lg hover:bg-violet-600"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
🧪 测试面板
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-24 left-3 z-[9999] w-72 rounded-xl border border-violet-200 bg-white/95 shadow-2xl backdrop-blur-sm">
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="flex items-center justify-between rounded-t-xl bg-violet-500 px-3 py-2">
|
||||||
|
<span className="text-xs font-bold text-white">🧪 iframe 通信测试</span>
|
||||||
|
<button
|
||||||
|
className="text-white/70 hover:text-white"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 p-3">
|
||||||
|
{/* 当前状态 */}
|
||||||
|
<div className="rounded-lg bg-gray-50 px-3 py-2 text-xs">
|
||||||
|
<div className="mb-1 font-semibold text-gray-500">当前状态</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span>
|
||||||
|
<span className="text-gray-400">mode:</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-mono font-bold",
|
||||||
|
isSkillMode ? "text-violet-600" : "text-gray-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSkillMode ? "skill ✅" : "普通"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="text-gray-400">selectedSkill:</span>
|
||||||
|
<span className="font-mono text-violet-600">
|
||||||
|
{iframeSkill.selectedSkill
|
||||||
|
? `${iframeSkill.selectedSkill.skill_id} / ${iframeSkill.selectedSkill.title}`
|
||||||
|
: "无"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 场景 1:侧边栏隐藏 */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold text-gray-500">
|
||||||
|
① 侧边栏隐藏(layout)
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleEnterSkillMode}
|
||||||
|
>
|
||||||
|
进入 skill 模式
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleExitSkillMode}
|
||||||
|
>
|
||||||
|
退出 skill 模式
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 场景 2:skill 选择通信 */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold text-gray-500">
|
||||||
|
② postMessage 通信(发送到宿主)
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-violet-50 text-xs text-violet-700 hover:bg-violet-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleSendSelectSkill}
|
||||||
|
>
|
||||||
|
sendSelectSkill (skill_001)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-violet-50 text-xs text-violet-700 hover:bg-violet-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleOpenSkillDialog}
|
||||||
|
>
|
||||||
|
openSkillDialog
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-red-50 text-xs text-red-600 hover:bg-red-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleClearSkill}
|
||||||
|
>
|
||||||
|
clearSkill (发送 skill_id=0)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 场景 3:接收宿主页 selectedSkill */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 text-xs font-semibold text-gray-500">
|
||||||
|
③ 接收宿主页 selectedSkill
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-green-50 text-xs text-green-700 hover:bg-green-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
window.postMessage(
|
||||||
|
{ type: "selectedSkill", id: 5, title: "文档处理" },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
addLog(
|
||||||
|
"模拟宿主页 → selectedSkill { id: 5, title: '文档处理' }",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✅ 模拟 selectedSkill(成功)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-orange-50 text-xs text-orange-700 hover:bg-orange-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
window.postMessage(
|
||||||
|
{ type: "selectedSkill", id: 999999, title: "不存在的技能" },
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
addLog(
|
||||||
|
"模拟宿主页 → selectedSkill { id: 999999, title: '不存在的技能' }",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
❌ 模拟 selectedSkill(失败/错误)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 场景 4:剪贴板复制(iframe 通信) */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 flex items-center justify-between">
|
||||||
|
<span className="text-xs font-semibold text-gray-500">
|
||||||
|
④ 剪贴板复制(iframe 通信)
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
|
isInIframe
|
||||||
|
? "bg-violet-100 text-violet-700"
|
||||||
|
: "bg-gray-100 text-gray-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isInIframe ? "iframe 模式" : "独立页面"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full bg-blue-50 text-xs text-blue-700 hover:bg-blue-100"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleTestClipboardCopy}
|
||||||
|
>
|
||||||
|
📋 测试复制到剪贴板
|
||||||
|
</Button>
|
||||||
|
<div className="rounded bg-gray-100 px-2 py-1.5 text-[10px] text-gray-600">
|
||||||
|
{isInIframe
|
||||||
|
? "将通过 postMessage 请求父页面复制"
|
||||||
|
: "将直接调用 navigator.clipboard"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 日志 */}
|
||||||
|
{log.length > 0 && (
|
||||||
|
<div className="rounded-lg bg-gray-900 p-2">
|
||||||
|
<div className="mb-1 text-[10px] font-semibold text-gray-400">
|
||||||
|
操作日志
|
||||||
|
</div>
|
||||||
|
{log.map((l, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="truncate font-mono text-[10px] text-green-400"
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +1,11 @@
|
||||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
import type { UseStream } 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 {
|
||||||
thread: BaseStream<AgentThreadState>;
|
threadId: string;
|
||||||
isMock?: boolean;
|
thread: UseStream<AgentThreadState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThreadContext = createContext<ThreadContextType | undefined>(
|
export const ThreadContext = createContext<ThreadContextType | undefined>(
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,16 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { AnchorHTMLAttributes } from "react";
|
import type { HTMLAttributes } 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;
|
||||||
|
|
@ -35,7 +30,7 @@ export function MarkdownContent({
|
||||||
}: MarkdownContentProps) {
|
}: MarkdownContentProps) {
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
a: (props: HTMLAttributes<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) {
|
||||||
|
|
@ -43,16 +38,7 @@ export function MarkdownContent({
|
||||||
return <CitationLink {...props}>{text}</CitationLink>;
|
return <CitationLink {...props}>{text}</CitationLink>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { className, target, rel, ...rest } = props;
|
return <a {...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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function MessageGroup({
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
return (
|
return (
|
||||||
<ChainOfThought
|
<ChainOfThought
|
||||||
className={cn("w-full gap-2 rounded-lg border p-0.5", className)}
|
className={cn("w-full gap-2 rounded-lg bg-white", className)}
|
||||||
open={true}
|
open={true}
|
||||||
>
|
>
|
||||||
{aboveLastToolCallSteps.length > 0 && (
|
{aboveLastToolCallSteps.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,26 @@
|
||||||
import type { Message } from "@langchain/langgraph-sdk";
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
import { FileIcon, Loader2Icon } from "lucide-react";
|
import { FileIcon } from "lucide-react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { memo, useMemo, type ImgHTMLAttributes } from "react";
|
import { memo, useMemo, useState, type ImgHTMLAttributes } from "react";
|
||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
|
|
||||||
import { Loader } from "@/components/ai-elements/loader";
|
|
||||||
import {
|
import {
|
||||||
Message as AIElementMessage,
|
Message as AIElementMessage,
|
||||||
MessageContent as AIElementMessageContent,
|
MessageContent as AIElementMessageContent,
|
||||||
MessageResponse as AIElementMessageResponse,
|
MessageResponse as AIElementMessageResponse,
|
||||||
MessageToolbar,
|
MessageToolbar,
|
||||||
} from "@/components/ai-elements/message";
|
} from "@/components/ai-elements/message";
|
||||||
import {
|
|
||||||
Reasoning,
|
|
||||||
ReasoningContent,
|
|
||||||
ReasoningTrigger,
|
|
||||||
} from "@/components/ai-elements/reasoning";
|
|
||||||
import { Task, TaskTrigger } from "@/components/ai-elements/task";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
extractReasoningContentFromMessage,
|
extractReasoningContentFromMessage,
|
||||||
parseUploadedFiles,
|
parseUploadedFiles,
|
||||||
stripUploadedFilesTag,
|
type UploadedFile,
|
||||||
type FileInMessage,
|
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
|
import { materializeSkillYaml } from "@/core/skills";
|
||||||
import { humanMessagePlugins } from "@/core/streamdown";
|
import { humanMessagePlugins } from "@/core/streamdown";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -47,7 +40,7 @@ export function MessageListItem({
|
||||||
const isHuman = message.type === "human";
|
const isHuman = message.type === "human";
|
||||||
return (
|
return (
|
||||||
<AIElementMessage
|
<AIElementMessage
|
||||||
className={cn("group/conversation-message relative w-full", className)}
|
className={cn("group/conversation-message relative w-full mb-1", className)}
|
||||||
from={isHuman ? "user" : "assistant"}
|
from={isHuman ? "user" : "assistant"}
|
||||||
>
|
>
|
||||||
<MessageContent
|
<MessageContent
|
||||||
|
|
@ -55,10 +48,9 @@ export function MessageListItem({
|
||||||
message={message}
|
message={message}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
{!isLoading && (
|
|
||||||
<MessageToolbar
|
<MessageToolbar
|
||||||
className={cn(
|
className={cn(
|
||||||
isHuman ? "-bottom-9 justify-end" : "-bottom-8",
|
isHuman ? "-bottom-8 justify-end" : "-bottom-8",
|
||||||
"absolute right-0 left-0 z-20 opacity-0 transition-opacity delay-200 duration-300 group-hover/conversation-message:opacity-100",
|
"absolute right-0 left-0 z-20 opacity-0 transition-opacity delay-200 duration-300 group-hover/conversation-message:opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -72,7 +64,6 @@ export function MessageListItem({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MessageToolbar>
|
</MessageToolbar>
|
||||||
)}
|
|
||||||
</AIElementMessage>
|
</AIElementMessage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -130,67 +121,37 @@ function MessageContent_({
|
||||||
|
|
||||||
const rawContent = extractContentFromMessage(message);
|
const rawContent = extractContentFromMessage(message);
|
||||||
const reasoningContent = extractReasoningContentFromMessage(message);
|
const reasoningContent = extractReasoningContentFromMessage(message);
|
||||||
|
const { contentToParse, uploadedFiles } = useMemo(() => {
|
||||||
const files = useMemo(() => {
|
if (!isLoading && reasoningContent && !rawContent) {
|
||||||
const files = message.additional_kwargs?.files;
|
return {
|
||||||
if (!Array.isArray(files) || files.length === 0) {
|
contentToParse: reasoningContent,
|
||||||
if (rawContent.includes("<uploaded_files>")) {
|
uploadedFiles: [] as UploadedFile[],
|
||||||
// If the content contains the <uploaded_files> tag, we return the parsed files from the content for backward compatibility.
|
};
|
||||||
return parseUploadedFiles(rawContent);
|
|
||||||
}
|
}
|
||||||
return null;
|
if (isHuman && rawContent) {
|
||||||
|
const { files, cleanContent: contentWithoutFiles } =
|
||||||
|
parseUploadedFiles(rawContent);
|
||||||
|
return { contentToParse: contentWithoutFiles, uploadedFiles: files };
|
||||||
}
|
}
|
||||||
return files as FileInMessage[];
|
return {
|
||||||
}, [message.additional_kwargs?.files, rawContent]);
|
contentToParse: rawContent ?? "",
|
||||||
|
uploadedFiles: [] as UploadedFile[],
|
||||||
const contentToDisplay = useMemo(() => {
|
};
|
||||||
if (isHuman) {
|
}, [isLoading, rawContent, reasoningContent, isHuman]);
|
||||||
return rawContent ? stripUploadedFilesTag(rawContent) : "";
|
|
||||||
}
|
|
||||||
return rawContent ?? "";
|
|
||||||
}, [rawContent, isHuman]);
|
|
||||||
|
|
||||||
const filesList =
|
const filesList =
|
||||||
files && files.length > 0 && thread_id ? (
|
uploadedFiles.length > 0 && thread_id ? (
|
||||||
<RichFilesList files={files} threadId={thread_id} />
|
<UploadedFilesList files={uploadedFiles} threadId={thread_id} />
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
// Uploading state: mock AI message shown while files upload
|
|
||||||
if (message.additional_kwargs?.element === "task") {
|
|
||||||
return (
|
|
||||||
<AIElementMessageContent className={className}>
|
|
||||||
<Task defaultOpen={false}>
|
|
||||||
<TaskTrigger title="">
|
|
||||||
<div className="text-muted-foreground flex w-full cursor-default items-center gap-2 text-sm select-none">
|
|
||||||
<Loader className="size-4" />
|
|
||||||
<span>{contentToDisplay}</span>
|
|
||||||
</div>
|
|
||||||
</TaskTrigger>
|
|
||||||
</Task>
|
|
||||||
</AIElementMessageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reasoning-only AI message (no main response content yet)
|
|
||||||
if (!isHuman && reasoningContent && !rawContent) {
|
|
||||||
return (
|
|
||||||
<AIElementMessageContent className={className}>
|
|
||||||
<Reasoning isStreaming={isLoading}>
|
|
||||||
<ReasoningTrigger />
|
|
||||||
<ReasoningContent>{reasoningContent}</ReasoningContent>
|
|
||||||
</Reasoning>
|
|
||||||
</AIElementMessageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
const messageResponse = contentToDisplay ? (
|
const messageResponse = contentToParse ? (
|
||||||
<AIElementMessageResponse
|
<AIElementMessageResponse
|
||||||
remarkPlugins={humanMessagePlugins.remarkPlugins}
|
remarkPlugins={humanMessagePlugins.remarkPlugins}
|
||||||
rehypePlugins={humanMessagePlugins.rehypePlugins}
|
rehypePlugins={humanMessagePlugins.rehypePlugins}
|
||||||
components={components}
|
components={components}
|
||||||
>
|
>
|
||||||
{contentToDisplay}
|
{contentToParse}
|
||||||
</AIElementMessageResponse>
|
</AIElementMessageResponse>
|
||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
|
|
@ -209,7 +170,7 @@ function MessageContent_({
|
||||||
<AIElementMessageContent className={className}>
|
<AIElementMessageContent className={className}>
|
||||||
{filesList}
|
{filesList}
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
content={contentToDisplay}
|
content={contentToParse}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
||||||
className="my-3"
|
className="my-3"
|
||||||
|
|
@ -262,32 +223,28 @@ function isImageFile(filename: string): boolean {
|
||||||
return IMAGE_EXTENSIONS.includes(getFileExt(filename));
|
return IMAGE_EXTENSIONS.includes(getFileExt(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function isYamlFile(filename: string): boolean {
|
||||||
* Format bytes to human-readable size string
|
const ext = getFileExt(filename);
|
||||||
*/
|
return ext === "yaml" || ext === "yml";
|
||||||
function formatBytes(bytes: number): string {
|
|
||||||
if (bytes === 0) return "—";
|
|
||||||
const kb = bytes / 1024;
|
|
||||||
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
||||||
return `${(kb / 1024).toFixed(1)} MB`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of files from additional_kwargs.files (with optional upload status)
|
* Uploaded files list component
|
||||||
*/
|
*/
|
||||||
function RichFilesList({
|
function UploadedFilesList({
|
||||||
files,
|
files,
|
||||||
threadId,
|
threadId,
|
||||||
}: {
|
}: {
|
||||||
files: FileInMessage[];
|
files: UploadedFile[];
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
if (files.length === 0) return null;
|
if (files.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex flex-wrap justify-end gap-2">
|
<div className="mb-2 flex flex-wrap justify-end gap-2">
|
||||||
{files.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<RichFileCard
|
<UploadedFileCard
|
||||||
key={`${file.filename}-${index}`}
|
key={`${file.path}-${index}`}
|
||||||
file={file}
|
file={file}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
/>
|
/>
|
||||||
|
|
@ -297,50 +254,48 @@ function RichFilesList({
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single file card that handles FileInMessage (supports uploading state)
|
* Single uploaded file card component
|
||||||
*/
|
*/
|
||||||
function RichFileCard({
|
function UploadedFileCard({
|
||||||
file,
|
file,
|
||||||
threadId,
|
threadId,
|
||||||
}: {
|
}: {
|
||||||
file: FileInMessage;
|
file: UploadedFile;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const [isMaterializing, setIsMaterializing] = useState(false);
|
||||||
const isUploading = file.status === "uploading";
|
const [materializeMessage, setMaterializeMessage] = useState<string | null>(
|
||||||
const isImage = isImageFile(file.filename);
|
null,
|
||||||
|
|
||||||
if (isUploading) {
|
|
||||||
return (
|
|
||||||
<div className="bg-background border-border/40 flex max-w-50 min-w-30 flex-col gap-1 rounded-lg border p-3 opacity-60 shadow-sm">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Loader2Icon className="text-muted-foreground mt-0.5 size-4 shrink-0 animate-spin" />
|
|
||||||
<span
|
|
||||||
className="text-foreground truncate text-sm font-medium"
|
|
||||||
title={file.filename}
|
|
||||||
>
|
|
||||||
{file.filename}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="rounded px-1.5 py-0.5 text-[10px] font-normal"
|
|
||||||
>
|
|
||||||
{getFileTypeLabel(file.filename)}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-muted-foreground text-[10px]">
|
|
||||||
{t.uploads.uploading}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.path) return null;
|
if (!threadId) return null;
|
||||||
|
|
||||||
|
const isImage = isImageFile(file.filename);
|
||||||
|
const isYaml = isYamlFile(file.filename);
|
||||||
const fileUrl = resolveArtifactURL(file.path, threadId);
|
const fileUrl = resolveArtifactURL(file.path, threadId);
|
||||||
|
|
||||||
|
const handleMaterializeYaml = async () => {
|
||||||
|
if (isMaterializing) return;
|
||||||
|
setIsMaterializing(true);
|
||||||
|
setMaterializeMessage(null);
|
||||||
|
try {
|
||||||
|
const result = await materializeSkillYaml({
|
||||||
|
thread_id: threadId,
|
||||||
|
path: file.path,
|
||||||
|
target_dir: "/mnt/user-data/uploads/skill",
|
||||||
|
clear_target: true,
|
||||||
|
});
|
||||||
|
setMaterializeMessage(
|
||||||
|
`已创建 ${result.created_files} 个文件 / ${result.created_directories} 个目录`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "解析失败";
|
||||||
|
setMaterializeMessage(`失败: ${message}`);
|
||||||
|
} finally {
|
||||||
|
setIsMaterializing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
|
@ -352,14 +307,14 @@ function RichFileCard({
|
||||||
<img
|
<img
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
alt={file.filename}
|
alt={file.filename}
|
||||||
className="h-32 w-auto max-w-60 object-cover transition-transform group-hover:scale-105"
|
className="h-32 w-auto max-w-[240px] object-cover transition-transform group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border-border/40 flex max-w-50 min-w-30 flex-col gap-1 rounded-lg border p-3 shadow-sm">
|
<div className="bg-background border-border/40 flex max-w-[200px] min-w-[120px] flex-col gap-1 rounded-lg border p-3 shadow-sm">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
<FileIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||||
<span
|
<span
|
||||||
|
|
@ -376,10 +331,29 @@ function RichFileCard({
|
||||||
>
|
>
|
||||||
{getFileTypeLabel(file.filename)}
|
{getFileTypeLabel(file.filename)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-muted-foreground text-[10px]">
|
<span className="text-muted-foreground text-[10px]">{file.size}</span>
|
||||||
{formatBytes(file.size)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* 注释掉测试按钮,后续根据需求再决定是否保留 */}
|
||||||
|
{/* {isYaml && (
|
||||||
|
<div className="mt-1 flex flex-col gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
void handleMaterializeYaml();
|
||||||
|
}}
|
||||||
|
disabled={isMaterializing}
|
||||||
|
>
|
||||||
|
{isMaterializing ? "解析中..." : "一键导入为 Skill 目录"}
|
||||||
|
</Button>
|
||||||
|
{materializeMessage && (
|
||||||
|
<span className="text-muted-foreground text-[10px] leading-tight">
|
||||||
|
{materializeMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue