Compare commits
5 Commits
f67aa27434
...
fb226f85a8
| Author | SHA1 | Date |
|---|---|---|
|
|
fb226f85a8 | |
|
|
6335424aca | |
|
|
4df604d491 | |
|
|
cb0ebf41bb | |
|
|
3a7940654c |
|
|
@ -0,0 +1,91 @@
|
|||
# DeerFlow Local Production Test
|
||||
# Usage: docker compose -f docker-compose-local-prod.yaml up -d
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Build images first:
|
||||
# docker build -f frontend/Dockerfile.prod -t deerflow-frontend:local .
|
||||
# docker build -f backend/Dockerfile -t deerflow-backend:local .
|
||||
#
|
||||
# Services:
|
||||
# - nginx: Reverse proxy (port 2026)
|
||||
# - frontend: Frontend Next.js (production build)
|
||||
# - gateway: Backend Gateway API
|
||||
# - langgraph: LangGraph server
|
||||
|
||||
name: deerflow2-local
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: deer-flow-nginx
|
||||
ports:
|
||||
- "2026:2026"
|
||||
volumes:
|
||||
- ./nginx/nginx-local-prod.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- frontend
|
||||
- gateway
|
||||
- langgraph
|
||||
networks:
|
||||
- deer-flow-local
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
image: deerflow-frontend:local
|
||||
container_name: deer-flow-frontend
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- deer-flow-local
|
||||
restart: unless-stopped
|
||||
|
||||
gateway:
|
||||
image: deerflow-backend:local
|
||||
container_name: deer-flow-gateway
|
||||
command: sh -c "cd backend && uv run uvicorn src.gateway.app:app --host 0.0.0.0 --port 8001"
|
||||
volumes:
|
||||
- ../config.yaml:/app/config.yaml
|
||||
- ../extensions_config.json:/app/extensions_config.json
|
||||
- ../skills:/app/skills
|
||||
- ../logs:/app/logs
|
||||
- ../backend/.deer-flow:/app/backend/.deer-flow
|
||||
- ~/.cache/uv:/root/.cache/uv
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- CI=true
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
env_file:
|
||||
- ../.env
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- deer-flow-local
|
||||
restart: unless-stopped
|
||||
|
||||
langgraph:
|
||||
image: deerflow-backend:local
|
||||
container_name: deer-flow-langgraph
|
||||
command: sh -c "cd backend && exec uv run langgraph dev --no-browser --no-reload --allow-blocking --host 0.0.0.0 --port 2024"
|
||||
volumes:
|
||||
- ../backend/.langgraph_api:/app/backend/.langgraph_api
|
||||
- ../config.yaml:/app/config.yaml
|
||||
- ../extensions_config.json:/app/extensions_config.json
|
||||
- ../skills:/app/skills
|
||||
- ../logs:/app/logs
|
||||
- ../backend/.deer-flow:/app/backend/.deer-flow
|
||||
- ~/.cache/uv:/root/.cache/uv
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- CI=true
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
env_file:
|
||||
- ../.env
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- deer-flow-local
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
deer-flow-local:
|
||||
driver: bridge
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
pid /tmp/nginx.pid;
|
||||
http {
|
||||
# Basic settings
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Logging
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr;
|
||||
|
||||
# Docker internal DNS (for resolving service names)
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Upstream servers (using Docker service names)
|
||||
upstream gateway {
|
||||
server gateway:8001;
|
||||
}
|
||||
|
||||
upstream langgraph {
|
||||
server langgraph:2024;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:3000;
|
||||
}
|
||||
|
||||
# ── Main server (path-based routing) ─────────────────────────────────
|
||||
server {
|
||||
listen 2026 default_server;
|
||||
listen [::]:2026 default_server;
|
||||
server_name _;
|
||||
|
||||
# Hide CORS headers from upstream to prevent duplicates
|
||||
proxy_hide_header 'Access-Control-Allow-Origin';
|
||||
proxy_hide_header 'Access-Control-Allow-Methods';
|
||||
proxy_hide_header 'Access-Control-Allow-Headers';
|
||||
proxy_hide_header 'Access-Control-Allow-Credentials';
|
||||
|
||||
# CORS headers for all responses (nginx handles CORS centrally)
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' '*' always;
|
||||
|
||||
# Handle OPTIONS requests (CORS preflight)
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
|
||||
# LangGraph API routes
|
||||
# Rewrites /api/langgraph/* to /* before proxying
|
||||
location /api/langgraph/ {
|
||||
rewrite ^/api/langgraph/(.*) /$1 break;
|
||||
proxy_pass http://langgraph;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection '';
|
||||
|
||||
# SSE/Streaming support
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_set_header X-Accel-Buffering no;
|
||||
|
||||
# Timeouts for long-running requests
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
|
||||
# Chunked transfer encoding
|
||||
chunked_transfer_encoding on;
|
||||
}
|
||||
|
||||
# Custom API: Models endpoint
|
||||
location /api/models {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Custom API: Memory endpoint
|
||||
location /api/memory {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Custom API: MCP configuration endpoint
|
||||
location /api/mcp {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Custom API: Skills configuration endpoint
|
||||
location /api/skills {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Custom API: Artifacts endpoint
|
||||
location ~ ^/api/threads/[^/]+/artifacts {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Custom API: Uploads endpoint
|
||||
location ~ ^/api/threads/[^/]+/uploads {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Large file upload support
|
||||
client_max_body_size 100M;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
# API Documentation: Swagger UI
|
||||
location /docs {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API Documentation: ReDoc
|
||||
location /redoc {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API Documentation: OpenAPI Schema
|
||||
location /openapi.json {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Health check endpoint (gateway)
|
||||
location /health {
|
||||
proxy_pass http://gateway;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# All other requests go to frontend
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# Headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
"build": "next build",
|
||||
"check": "next lint && tsc --noEmit",
|
||||
"dev": "next dev --turbo",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"preview": "next build && next start",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default async function RootLayout({
|
|||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={geist.variable}
|
||||
className={geist.variable + ""}
|
||||
suppressContentEditableWarning
|
||||
suppressHydrationWarning
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ import {
|
|||
DevDialogHeader,
|
||||
DevDialogTitle,
|
||||
} from "@/components/ui/dev-dialog";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import {
|
||||
ArtifactFileDetail,
|
||||
|
|
@ -32,7 +27,6 @@ import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
|||
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
||||
import { InputBox } from "@/components/workspace/input-box";
|
||||
import { MessageList } from "@/components/workspace/messages";
|
||||
import { MessageListSkeleton } from "@/components/workspace/messages/skeleton";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
|
|
@ -131,6 +125,10 @@ export default function ChatPage() {
|
|||
fetchStateHistory: true,
|
||||
onFinish: (state) => {
|
||||
setFinalState(state);
|
||||
// 新对话完成后导航到对话页面
|
||||
if (isNewThread && threadId) {
|
||||
router.push(pathOfThread(threadId));
|
||||
}
|
||||
if (document.hidden || !document.hasFocus()) {
|
||||
let body = "Conversation finished";
|
||||
const lastMessage = state.messages[state.messages.length - 1];
|
||||
|
|
@ -166,10 +164,6 @@ export default function ChatPage() {
|
|||
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
||||
const suppressNewThreadSubmitUi =
|
||||
isNewThread && createNewSession && hasSubmitted;
|
||||
const suppressConversationUi =
|
||||
suppressExistingThreadPrefetchUi || suppressNewThreadSubmitUi;
|
||||
|
||||
useEffect(() => {
|
||||
const pageTitle = isNewThread
|
||||
|
|
@ -177,7 +171,7 @@ export default function ChatPage() {
|
|||
: thread.values?.title && thread.values.title !== "Untitled"
|
||||
? thread.values.title
|
||||
: t.pages.untitled;
|
||||
if (thread.isThreadLoading && !suppressConversationUi) {
|
||||
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
|
||||
document.title = `Loading... - ${t.pages.appName}`;
|
||||
} else {
|
||||
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||
|
|
@ -189,21 +183,19 @@ export default function ChatPage() {
|
|||
t.pages.appName,
|
||||
thread.values.title,
|
||||
thread.isThreadLoading,
|
||||
suppressConversationUi,
|
||||
suppressExistingThreadPrefetchUi,
|
||||
]);
|
||||
|
||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||
useEffect(() => {
|
||||
if (!suppressConversationUi) {
|
||||
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]!);
|
||||
}
|
||||
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]!);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
@ -237,7 +229,7 @@ export default function ChatPage() {
|
|||
subagent_enabled: settings.context.mode === "ultra",
|
||||
},
|
||||
afterSubmit() {
|
||||
router.push(pathOfThread(threadId!));
|
||||
// 导航已在 onFinish 中处理,确保 stream 完成后再导航
|
||||
},
|
||||
});
|
||||
const handleSubmit = useCallback(
|
||||
|
|
@ -260,290 +252,300 @@ export default function ChatPage() {
|
|||
|
||||
return (
|
||||
<ThreadContext.Provider value={{ threadId, thread }}>
|
||||
<ResizablePanelGroup orientation="horizontal">
|
||||
<ResizablePanel
|
||||
className="relative overflow-hidden rounded-[20px]"
|
||||
defaultSize={artifactPanelOpen ? 46 : 100}
|
||||
minSize={artifactPanelOpen ? 30 : 100}
|
||||
>
|
||||
<div className="relative flex size-full min-h-0 justify-between rounded-[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 ? "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"
|
||||
<div
|
||||
className={cn(
|
||||
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
||||
artifactsOpen ? "w-full" : "w-[70%]",
|
||||
)}
|
||||
>
|
||||
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||
<div
|
||||
className={cn(
|
||||
"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)}
|
||||
>
|
||||
<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-medium">
|
||||
{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 ?? []}
|
||||
hidden={
|
||||
!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]"
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<ListTodoIcon className="size-4" /> To-dos
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<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 ?? []}
|
||||
hidden={
|
||||
!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>
|
||||
{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>
|
||||
</header>
|
||||
<main
|
||||
className={cn(
|
||||
"flex min-h-0 max-w-full grow flex-col rounded-[20px]",
|
||||
isNewThread ? "bg-white" : "bg-background",
|
||||
)}
|
||||
>
|
||||
<div className="flex size-full justify-center">
|
||||
{suppressConversationUi ? (
|
||||
<MessageListSkeleton />
|
||||
) : (
|
||||
>
|
||||
<div className="flex size-full justify-center">
|
||||
<MessageList
|
||||
className={cn("size-full", !isNewThread && "pt-10")}
|
||||
className={cn(
|
||||
"size-full",
|
||||
(!isNewThread || hasSubmitted) && "pt-[20px]",
|
||||
)}
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
suppressThreadLoading={suppressExistingThreadPrefetchUi}
|
||||
messagesOverride={
|
||||
!thread.isLoading && finalState?.messages
|
||||
? (finalState.messages as Message[])
|
||||
: undefined
|
||||
suppressExistingThreadPrefetchUi
|
||||
? []
|
||||
: !thread.isLoading && finalState?.messages
|
||||
? (finalState.messages as Message[])
|
||||
: undefined
|
||||
}
|
||||
paddingBottom={todoListCollapsed ? 160 : 280}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
<div
|
||||
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-[70%]"
|
||||
: "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}
|
||||
/>
|
||||
) : (
|
||||
<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(
|
||||
"opacity-33 hover:opacity-100",
|
||||
!artifactPanelOpen && "pointer-events-none opacity-0",
|
||||
"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" : "",
|
||||
)}
|
||||
/>
|
||||
<ResizablePanel
|
||||
className={cn(
|
||||
"bg-background ml-[20px] rounded-[20px] transition-all duration-300 ease-in-out",
|
||||
!artifactsOpen && "opacity-0",
|
||||
)}
|
||||
defaultSize={artifactPanelOpen ? 64 : 0}
|
||||
minSize={0}
|
||||
maxSize={artifactPanelOpen ? undefined : 0}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-full transition-transform duration-300 ease-in-out",
|
||||
artifactPanelOpen ? "translate-x-0" : "translate-x-full",
|
||||
"pointer-events-auto relative w-full max-w-[720px]",
|
||||
isNewThread && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
||||
)}
|
||||
>
|
||||
{selectedArtifact ? (
|
||||
<ArtifactFileDetail
|
||||
className="size-full"
|
||||
filepath={selectedArtifact}
|
||||
threadId={threadId}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative flex size-full justify-center px-[20px]">
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setArtifactsOpen(false);
|
||||
}}
|
||||
>
|
||||
<XIcon />
|
||||
</Button>
|
||||
<InputBox
|
||||
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
||||
isNewThread={isNewThread}
|
||||
hasSubmitted={hasSubmitted}
|
||||
autoFocus={isNewThread}
|
||||
status={
|
||||
suppressExistingThreadPrefetchUi
|
||||
? "ready"
|
||||
: thread.isLoading
|
||||
? "streaming"
|
||||
: "ready"
|
||||
}
|
||||
context={settings.context}
|
||||
extraHeader={
|
||||
<div className="flex flex-col gap-4">
|
||||
{isNewThread && !hasSubmitted && (
|
||||
<Welcome mode={settings.context.mode} />
|
||||
)}
|
||||
</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">
|
||||
{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>
|
||||
)}
|
||||
}
|
||||
disabled={
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||
isSelectedSkillBootstrapping
|
||||
}
|
||||
onContextChange={(context) => setSettings("context", context)}
|
||||
onSubmit={handleSubmit}
|
||||
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" && (
|
||||
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
||||
{t.common.notAvailableInDemoMode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
{/* 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 ? "right-[50%]" : "",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto relative w-full max-w-[720px]",
|
||||
isNewThread && "top-[-65px] -translate-y-[calc(50vh-96px)]",
|
||||
)}
|
||||
>
|
||||
<InputBox
|
||||
className={cn("w-full rounded-[20px] bg-[#FBFAFC]")}
|
||||
isNewThread={isNewThread}
|
||||
autoFocus={isNewThread}
|
||||
status={
|
||||
suppressExistingThreadPrefetchUi
|
||||
? "ready"
|
||||
: thread.isLoading
|
||||
? "streaming"
|
||||
: "ready"
|
||||
}
|
||||
context={settings.context}
|
||||
extraHeader={
|
||||
<div className="flex flex-col gap-4">
|
||||
{isNewThread && <Welcome mode={settings.context.mode} />}
|
||||
</div>
|
||||
}
|
||||
disabled={
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
|
||||
isSelectedSkillBootstrapping
|
||||
}
|
||||
onContextChange={(context) => setSettings("context", context)}
|
||||
onSubmit={handleSubmit}
|
||||
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" && (
|
||||
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
||||
{t.common.notAvailableInDemoMode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 退出确认对话框 */}
|
||||
<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={() => {
|
||||
setShowExitDialog(false);
|
||||
router.push("/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>
|
||||
|
||||
{/* 退出确认对话框 */}
|
||||
<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={() => {
|
||||
setShowExitDialog(false);
|
||||
router.push("/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 /> */}
|
||||
</ThreadContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
|||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background flex flex-col overflow-hidden rounded-[20px] px-[20px] pt-[15px]",
|
||||
"bg-background flex flex-col overflow-hidden rounded-t-[20px] px-[20px] pt-[15px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -30,10 +30,7 @@ export const ArtifactHeader = ({
|
|||
...props
|
||||
}: ArtifactHeaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-[20px] grid grid-cols-3 items-center justify-between",
|
||||
className,
|
||||
)}
|
||||
className={cn("mb-[10px] flex items-center justify-between", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
@ -144,6 +141,6 @@ export const ArtifactContent = ({
|
|||
...props
|
||||
}: ArtifactContentProps) => (
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<div className={cn("mb-[208px] p-4", className)} {...props} />
|
||||
<div className={cn("mb-[150px] p-4", className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export const ChainOfThoughtStep = memo(
|
|||
{isValidElement(Icon) ? (
|
||||
Icon
|
||||
) : (
|
||||
<Icon className="size-4 stroke-[1.5px] stroke-[#333333] text-[#333333]" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|||
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
||||
className={cn("relative flex-1 overflow-y-hidden mt-[60px]", className)}
|
||||
initial="smooth"
|
||||
resize="smooth"
|
||||
role="log"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
|
|||
className={cn(
|
||||
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
||||
from === "user"
|
||||
? "is-user ml-auto justify-end"
|
||||
? "is-user px-0 ml-auto justify-end"
|
||||
: "is-assistant bg-[#ffffff]",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export const Suggestion = ({
|
|||
<Button
|
||||
className={cn(
|
||||
"cursor-pointer rounded-full px-[20px] py-[15px] text-xs font-normal",
|
||||
"bg-[#F9F8FA] text-[#666666] border-none",
|
||||
"border-none bg-[#F9F8FA] text-[#666666]",
|
||||
"hover:bg-[#EAE9EB] hover:text-[#150033]",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface AuroraTextProps {
|
|||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
|
|
@ -15,6 +16,7 @@ export const AuroraText = memo(
|
|||
className = "",
|
||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||
speed = 1,
|
||||
style,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||
|
|
@ -26,7 +28,7 @@ export const AuroraText = memo(
|
|||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className={`relative inline-block ${className}`} style={style}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -19,6 +21,46 @@ interface DropdownSelectorProps<T extends 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,
|
||||
|
|
@ -27,16 +69,20 @@ export function DropdownSelector<T extends string>({
|
|||
contentClassName,
|
||||
}: DropdownSelectorProps<T>) {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={
|
||||
triggerClassName ??
|
||||
"border-none bg-transparent shadow-none select-none focus:outline-none"
|
||||
}
|
||||
>
|
||||
{selectedOption?.label ?? value}
|
||||
<span className="flex items-center gap-1">
|
||||
{selectedOption?.label ?? value}
|
||||
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className={contentClassName}>
|
||||
<DropdownMenuRadioGroup
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group dark:bg-background/80 relative flex w-full max-w-[720px] items-center overflow-hidden rounded-md bg-[#FBFAFC] shadow-[0_0_20px_0_rgba(0,0,0,0.10)] 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",
|
||||
|
||||
// Variants based on alignment.
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ function ToggleGroupItem({
|
|||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 cursor-pointer 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",
|
||||
"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]: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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { CodeEditor } from "@/components/workspace/code-editor";
|
|||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { installSkill } from "@/core/skills/api";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
|
|
@ -64,8 +65,8 @@ export function ArtifactFileDetail({
|
|||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
|
||||
useArtifacts();
|
||||
const isWriteFile = useMemo(() => {
|
||||
return filepathFromProps.startsWith("write-file:");
|
||||
}, [filepathFromProps]);
|
||||
|
|
@ -111,33 +112,17 @@ export function ArtifactFileDetail({
|
|||
|
||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [zoom, setZoom] = useState(80);
|
||||
|
||||
// 全屏切换处理
|
||||
const handleFullscreenToggle = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch((err) => {
|
||||
console.error("无法进入全屏模式:", err);
|
||||
});
|
||||
setFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen().catch((err) => {
|
||||
console.error("无法退出全屏模式:", err);
|
||||
});
|
||||
setFullscreen(false);
|
||||
}
|
||||
}, [setFullscreen]);
|
||||
|
||||
// 监听全屏变化
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setFullscreen(!!document.fullscreenElement);
|
||||
};
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
};
|
||||
}, [setFullscreen]);
|
||||
const newFullscreen = !fullscreen;
|
||||
setFullscreen(newFullscreen);
|
||||
sendToParent({
|
||||
type: POST_MESSAGE_TYPES.FULLSCREEN,
|
||||
fullscreen: newFullscreen,
|
||||
});
|
||||
}, [fullscreen, setFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewable) {
|
||||
|
|
@ -175,8 +160,9 @@ export function ArtifactFileDetail({
|
|||
{previewable && (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant={null}
|
||||
size="default"
|
||||
className="h-[28px]"
|
||||
value={viewMode}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
|
|
@ -185,13 +171,52 @@ export function ArtifactFileDetail({
|
|||
}}
|
||||
>
|
||||
<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"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11 3L7 15"
|
||||
stroke="#150033"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 6L16 9L13 12"
|
||||
stroke="#150033"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ToggleGroupItem>
|
||||
<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>
|
||||
</ToggleGroup>
|
||||
)}
|
||||
{/* 放大缩小选择器 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
</div>
|
||||
<div className="flex min-w-0 grow items-center justify-center">
|
||||
<ArtifactTitle>
|
||||
|
|
@ -207,8 +232,6 @@ export function ArtifactFileDetail({
|
|||
</ArtifactTitle>
|
||||
</div>
|
||||
<div className="flex items-center justify-end overflow-hidden">
|
||||
{/* 放大缩小选择器 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
<ArtifactActions>
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
|
|
@ -340,25 +363,27 @@ export function ArtifactFileDetail({
|
|||
</svg>
|
||||
)}
|
||||
</ArtifactAction>
|
||||
<ArtifactAction
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip={t.common.close}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{!fullscreen && (
|
||||
<ArtifactAction
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip={t.common.close}
|
||||
>
|
||||
<path
|
||||
d="M4 14L14 4M4 4L14 14"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
<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"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
)}
|
||||
</ArtifactActions>
|
||||
</div>
|
||||
</ArtifactHeader>
|
||||
|
|
@ -471,8 +496,7 @@ export const ArtifactZoomSelector = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-[10px] bg-white px-2 py-1 backdrop-blur-sm",
|
||||
"border border-gray-200/50",
|
||||
"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,
|
||||
)}
|
||||
|
|
@ -483,18 +507,36 @@ export const ArtifactZoomSelector = ({
|
|||
onClick={handleZoomIn}
|
||||
disabled={!canZoomIn}
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
||||
"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="放大"
|
||||
>
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
<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"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-[42px] text-center text-xs font-medium text-gray-600",
|
||||
"min-w-[36px] text-center text-xs font-medium text-gray-600",
|
||||
"dark:text-gray-300",
|
||||
)}
|
||||
>
|
||||
|
|
@ -505,14 +547,32 @@ export const ArtifactZoomSelector = ({
|
|||
onClick={handleZoomOut}
|
||||
disabled={!canZoomOut}
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
||||
"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="缩小"
|
||||
>
|
||||
<ZoomOut className="h-3.5 w-3.5" />
|
||||
<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"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export function InputBox({
|
|||
context,
|
||||
extraHeader,
|
||||
isNewThread,
|
||||
hasSubmitted,
|
||||
initialValue,
|
||||
onContextChange,
|
||||
onSubmit,
|
||||
|
|
@ -107,6 +108,7 @@ export function InputBox({
|
|||
};
|
||||
extraHeader?: React.ReactNode;
|
||||
isNewThread?: boolean;
|
||||
hasSubmitted?: boolean;
|
||||
initialValue?: string;
|
||||
onContextChange?: (
|
||||
context: Omit<
|
||||
|
|
@ -139,8 +141,9 @@ export function InputBox({
|
|||
);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// isNewThread 时禁用收缩,始终保持展开
|
||||
const effectiveIsFocused = (isNewThread ?? false) || isFocused;
|
||||
// isNewThread 时禁用收缩,始终保持展开(除非已提交消息)
|
||||
const effectiveIsFocused =
|
||||
((isNewThread ?? false) && !hasSubmitted) || isFocused;
|
||||
|
||||
// 点击外部区域时收起输入框
|
||||
useEffect(() => {
|
||||
|
|
@ -302,7 +305,7 @@ export function InputBox({
|
|||
)}
|
||||
inputGroupClassName={cn(
|
||||
"border-0 rounded-[20px] backdrop-blur-sm",
|
||||
"transition-[height] duration-300 ease-out",
|
||||
"transition-[height] duration-300 ease-out shadow-none ",
|
||||
!isNewThread && "h-[200px] shadow-[0_0_20px_0_rgba(0,0,0,0.10)]",
|
||||
effectiveIsFocused ? "h-[200px]" : "h-[80px]",
|
||||
)}
|
||||
|
|
@ -376,7 +379,7 @@ export function InputBox({
|
|||
/>
|
||||
</PromptInput>
|
||||
|
||||
{isNewThread && searchParams.get("mode") !== "skill" && (
|
||||
{isNewThread && !hasSubmitted && searchParams.get("mode") !== "skill" && (
|
||||
<SuggestionListContainer
|
||||
sendSelectSkill={iframeSkill.sendSelectSkill}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function MessageList({
|
|||
<Conversation
|
||||
className={cn("flex size-full flex-col justify-center", className)}
|
||||
>
|
||||
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-8 pt-12">
|
||||
<ConversationContent className="w-full gap-8 px-[20px]">
|
||||
{groupMessages(messages, (group) => {
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -41,7 +41,14 @@ export function Welcome({
|
|||
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<AuroraText className="text-[18px] text-[#150033]" colors={colors}>
|
||||
<AuroraText
|
||||
className="text-center font-normal text-[18px] leading-normal"
|
||||
style={{
|
||||
color: "var(--color-150033, #150033)",
|
||||
fontFamily: '"Microsoft YaHei"',
|
||||
}}
|
||||
colors={colors}
|
||||
>
|
||||
{t.welcome.greeting}
|
||||
</AuroraText>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const zhCN: Translations = {
|
|||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: "使用Skill",
|
||||
greeting: "使用 Skill",
|
||||
description:
|
||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
||||
|
||||
|
|
@ -104,36 +104,42 @@ export const zhCN: Translations = {
|
|||
followupConfirmReplace: "替换并发送",
|
||||
suggestions: [
|
||||
{
|
||||
suggestion: "论文写作",
|
||||
suggestion: "自媒体文案",
|
||||
prompt:
|
||||
"撰写一篇关于[主题]的学术论文,包含摘要、引言、正文和参考文献。",
|
||||
"为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。",
|
||||
icon: PenLineIcon,
|
||||
skill_id: "1",
|
||||
skill_id: "432",
|
||||
},
|
||||
{
|
||||
suggestion: "报告生成",
|
||||
prompt: "深入分析[主题],生成一份结构清晰的调研报告。",
|
||||
icon: MicroscopeIcon,
|
||||
skill_id: "2",
|
||||
suggestion: "需求文档",
|
||||
prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。",
|
||||
icon: CompassIcon,
|
||||
skill_id: "521",
|
||||
},
|
||||
{
|
||||
suggestion: "策划文案",
|
||||
prompt: "为[项目/活动]撰写一份完整的策划方案和宣传文案。",
|
||||
icon: ShapesIcon,
|
||||
skill_id: "3",
|
||||
suggestion: "使用指南",
|
||||
prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。",
|
||||
icon: GraduationCapIcon,
|
||||
skill_id: "410",
|
||||
},
|
||||
{
|
||||
suggestion: "PPT生成",
|
||||
prompt: "生成一个关于[主题]的PPT演示文稿大纲和内容。",
|
||||
icon: GraduationCapIcon,
|
||||
skill_id: "4",
|
||||
skill_id: "180",
|
||||
},
|
||||
{
|
||||
suggestion: "文档处理",
|
||||
prompt: "对[文档]进行阅读、总结、翻译或格式转换等处理。",
|
||||
icon: CompassIcon,
|
||||
suggestion: "Excel数据分析",
|
||||
prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。",
|
||||
icon: MicroscopeIcon,
|
||||
skill_id: "5",
|
||||
},
|
||||
{
|
||||
suggestion: "市场调研",
|
||||
prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。",
|
||||
icon: ShapesIcon,
|
||||
skill_id: "31",
|
||||
},
|
||||
],
|
||||
suggestionsCreate: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* iframe 与宿主页通信消息类型常量
|
||||
*
|
||||
* 消息格式:{ type: MESSAGE_TYPE, ...其他字段 }
|
||||
* 发送方式:window.parent.postMessage(message, "*")
|
||||
*/
|
||||
|
||||
// 发送给宿主页的消息类型
|
||||
export const POST_MESSAGE_TYPES = {
|
||||
// 全屏切换
|
||||
FULLSCREEN: "fullscreen",
|
||||
// 选择预定义 skill
|
||||
SELECT_SKILL: "selectSkill",
|
||||
// 打开 skill 选择对话框
|
||||
OPEN_SKILL_DIALOG: "openSkillDialog",
|
||||
} as const;
|
||||
|
||||
// 接收来自宿主页的消息类型
|
||||
export const RECEIVE_MESSAGE_TYPES = {
|
||||
// 选中的 skill 数据
|
||||
SELECTED_SKILL: "selectedSkill",
|
||||
} as const;
|
||||
|
||||
// 消息类型
|
||||
export type PostMessageType =
|
||||
(typeof POST_MESSAGE_TYPES)[keyof typeof POST_MESSAGE_TYPES];
|
||||
export type ReceiveMessageType =
|
||||
(typeof RECEIVE_MESSAGE_TYPES)[keyof typeof RECEIVE_MESSAGE_TYPES];
|
||||
|
||||
// 消息数据类型
|
||||
export interface FullscreenMessage {
|
||||
type: typeof POST_MESSAGE_TYPES.FULLSCREEN;
|
||||
fullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface SelectSkillMessage {
|
||||
type: typeof POST_MESSAGE_TYPES.SELECT_SKILL;
|
||||
skill_id: string;
|
||||
}
|
||||
|
||||
export interface OpenSkillDialogMessage {
|
||||
type: typeof POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG;
|
||||
openSkillDialog: true;
|
||||
}
|
||||
|
||||
export interface SelectedSkillMessage {
|
||||
type: typeof RECEIVE_MESSAGE_TYPES.SELECTED_SKILL;
|
||||
id: string | number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
// 发送消息的辅助函数
|
||||
export function sendToParent(
|
||||
message: FullscreenMessage | SelectSkillMessage | OpenSkillDialogMessage,
|
||||
): void {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(message, "*");
|
||||
}
|
||||
}
|
||||
|
|
@ -18,29 +18,6 @@ import type {
|
|||
AgentThreadState,
|
||||
} from "./types";
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function waitForThreadStateToBeReadable(
|
||||
apiClient: ReturnType<typeof getAPIClient>,
|
||||
threadId: string,
|
||||
timeoutMs = 3000,
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const state = await apiClient.threads.getState<AgentThreadState>(threadId);
|
||||
if ((state.values.messages?.length ?? 0) > 0) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient 404 / not-ready errors while the new thread is being persisted.
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
export function useThreadStream({
|
||||
threadId,
|
||||
isNewThread,
|
||||
|
|
@ -212,10 +189,6 @@ export function useSubmitThread({
|
|||
},
|
||||
);
|
||||
|
||||
if (createNewSession && isNewThread && threadId) {
|
||||
await waitForThreadStateToBeReadable(apiClient, threadId);
|
||||
}
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
afterSubmit?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// 消息类型常量
|
||||
const MESSAGE_TYPES = {
|
||||
SELECT_SKILL: "selectSkill",
|
||||
OPEN_SKILL_DIALOG: "openSkillDialog",
|
||||
} as const;
|
||||
import {
|
||||
POST_MESSAGE_TYPES,
|
||||
RECEIVE_MESSAGE_TYPES,
|
||||
sendToParent,
|
||||
type SelectedSkillMessage,
|
||||
} from "@/core/iframe-messages";
|
||||
|
||||
// Skill 数据类型
|
||||
interface SkillData {
|
||||
|
|
@ -38,8 +39,8 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
|||
// 2. 监听宿主页 postMessage
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data?.type === "selectedSkill") {
|
||||
const { id, title } = event.data;
|
||||
if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
||||
const { id, title } = event.data as SelectedSkillMessage;
|
||||
setSelectedSkill({ skill_id: String(id), title });
|
||||
}
|
||||
};
|
||||
|
|
@ -49,28 +50,28 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
|||
|
||||
// 发送选择预定义 skill
|
||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||
const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||
console.log("[useIframeSkill] sendSelectSkill:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
sendToParent(message);
|
||||
}, []);
|
||||
|
||||
// 打开 skill 选择对话框
|
||||
const openSkillDialog = useCallback(() => {
|
||||
const message = {
|
||||
type: MESSAGE_TYPES.OPEN_SKILL_DIALOG,
|
||||
type: POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG,
|
||||
openSkillDialog: true,
|
||||
};
|
||||
} as const;
|
||||
console.log("[useIframeSkill] openSkillDialog:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
sendToParent(message);
|
||||
}, []);
|
||||
|
||||
// 清除选中并发送 skill_id=0 给主页
|
||||
const clearSkill = useCallback(() => {
|
||||
setSelectedSkill(null);
|
||||
// 发送 skill_id=0 给主页,通知取消选择
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" };
|
||||
const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" };
|
||||
console.log("[useIframeSkill] clearSkill, sending skill_id=0:", message);
|
||||
window.parent.postMessage(message, "*");
|
||||
sendToParent(message);
|
||||
}, []);
|
||||
|
||||
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ export function useSelectedSkillListener({
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(`[useSelectedSkillListener] 开始初始化技能: ${title} (${id})`);
|
||||
console.log(
|
||||
`[useSelectedSkillListener] 开始初始化技能: ${title} (${id})`,
|
||||
);
|
||||
setIsBootstrapping(true);
|
||||
toast.loading(`正在加载技能「${title}」...`, { id: "skill-bootstrap" });
|
||||
|
||||
|
|
@ -88,7 +90,8 @@ export function useSelectedSkillListener({
|
|||
if (result.success) {
|
||||
skillBootstrappedKeyRef.current = initKey;
|
||||
toast.success(`技能「${title}」加载成功`, {
|
||||
description: result.message || `已创建 ${result.created_files} 个文件`,
|
||||
description:
|
||||
result.message || `已创建 ${result.created_files} 个文件`,
|
||||
duration: 4000,
|
||||
});
|
||||
} else {
|
||||
|
|
@ -127,7 +130,10 @@ export function useSelectedSkillListener({
|
|||
if (data?.type !== "selectedSkill") return;
|
||||
|
||||
const { id, title } = data;
|
||||
console.log("[useSelectedSkillListener] 收到 postMessage selectedSkill:", data);
|
||||
console.log(
|
||||
"[useSelectedSkillListener] 收到 postMessage selectedSkill:",
|
||||
data,
|
||||
);
|
||||
|
||||
setSelectedSkill({ skill_id: String(id), title });
|
||||
void performBootstrap(id, title);
|
||||
|
|
|
|||
Loading…
Reference in New Issue