feat():detail界面换行;监听XClawUsed,如果有就用thread_id替换嵌入页路由;
This commit is contained in:
parent
724a5aca31
commit
7fe6d57316
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "@/components/workspace/artifacts";
|
||||
import { useThreadChat } from "@/components/workspace/chats";
|
||||
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 { ThreadContext } from "@/components/workspace/messages/context";
|
||||
|
|
@ -29,6 +30,7 @@ import { Tooltip } from "@/components/workspace/tooltip";
|
|||
import { useSpecificChatMode } from "@/components/workspace/use-chat-mode";
|
||||
import { Welcome } from "@/components/workspace/welcome";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { useNotification } from "@/core/notification/hooks";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
|
|
@ -412,6 +414,10 @@ export default function ChatPage() {
|
|||
await handleStop();
|
||||
}
|
||||
setShowExitDialog(false);
|
||||
sendToParent({
|
||||
type: POST_MESSAGE_TYPES.XCLAW_USED,
|
||||
XClawUsed: false,
|
||||
});
|
||||
// 使用完整页面刷新确保组件重新挂载,isNewThread 为 true
|
||||
window.location.href = "/workspace/chats/new";
|
||||
}}
|
||||
|
|
@ -450,8 +456,8 @@ export default function ChatPage() {
|
|||
</DevDialogContent>
|
||||
</DevDialog>
|
||||
|
||||
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||
{/* <IframeTestPanel /> */}
|
||||
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||
{process.env.NODE_ENV !== "production" && <IframeTestPanel />}
|
||||
</div>
|
||||
</ThreadContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState, type PointerEvent } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
||||
import { copyToClipboard } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -22,6 +23,13 @@ export function IframeTestPanel() {
|
|||
const iframeSkill = useIframeSkill();
|
||||
const [log, setLog] = useState<string[]>([]);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [position, setPosition] = useState<{ x: number; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragOffsetRef = useRef({ x: 0, y: 0 });
|
||||
const panelSizeRef = useRef({ width: 0, height: 0 });
|
||||
|
||||
const isSkillMode = searchParams.get("mode") === "skill";
|
||||
|
||||
|
|
@ -59,16 +67,68 @@ export function IframeTestPanel() {
|
|||
|
||||
function handleTestClipboardCopy() {
|
||||
const testText = "测试复制内容 - " + new Date().toISOString();
|
||||
copyToClipboard(testText);
|
||||
void copyToClipboard(testText);
|
||||
addLog(`copyToClipboard → "${testText.slice(0, 30)}..."`);
|
||||
}
|
||||
|
||||
function handleSendXClawUsed(used: boolean) {
|
||||
sendToParent({
|
||||
type: POST_MESSAGE_TYPES.XCLAW_USED,
|
||||
XClawUsed: used,
|
||||
});
|
||||
addLog(`postMessage → XClawUsed (${used})`);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
||||
if (!panelRef.current) return;
|
||||
const rect = panelRef.current.getBoundingClientRect();
|
||||
panelSizeRef.current = { width: rect.width, height: rect.height };
|
||||
dragOffsetRef.current = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
setPosition({ x: rect.left, y: rect.top });
|
||||
setDragging(true);
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const handleMove = (event: PointerEvent) => {
|
||||
const { width, height } = panelSizeRef.current;
|
||||
const nextX = event.clientX - dragOffsetRef.current.x;
|
||||
const nextY = event.clientY - dragOffsetRef.current.y;
|
||||
const clampedX = Math.min(
|
||||
Math.max(8, nextX),
|
||||
Math.max(8, window.innerWidth - width - 8),
|
||||
);
|
||||
const clampedY = Math.min(
|
||||
Math.max(8, nextY),
|
||||
Math.max(8, window.innerHeight - height - 8),
|
||||
);
|
||||
setPosition({ x: clampedX, y: clampedY });
|
||||
};
|
||||
const handleUp = () => {
|
||||
setDragging(false);
|
||||
};
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
window.addEventListener("pointerup", handleUp);
|
||||
return () => {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
};
|
||||
}, [dragging]);
|
||||
|
||||
// 检测是否在 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"
|
||||
className={cn(
|
||||
"fixed z-[9999] rounded-full bg-violet-500 px-3 py-1 text-xs font-bold text-white shadow-lg hover:bg-violet-600",
|
||||
position ? "left-0 top-0" : "bottom-24 left-3",
|
||||
)}
|
||||
style={position ? { left: position.x, top: position.y } : undefined}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
🧪 测试面板
|
||||
|
|
@ -76,9 +136,22 @@ export function IframeTestPanel() {
|
|||
);
|
||||
}
|
||||
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
|
||||
ref={panelRef}
|
||||
className={cn(
|
||||
"fixed z-[9999] w-72 rounded-xl border border-violet-200 bg-white/95 shadow-2xl backdrop-blur-sm",
|
||||
position ? "left-0 top-0" : "bottom-24 left-3",
|
||||
)}
|
||||
style={position ? { left: position.x, top: position.y } : undefined}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between rounded-t-xl bg-violet-500 px-3 py-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-grab items-center justify-between rounded-t-xl bg-violet-500 px-3 py-2 select-none",
|
||||
dragging && "cursor-grabbing",
|
||||
)}
|
||||
onPointerDown={handlePointerDown}
|
||||
>
|
||||
<span className="text-xs font-bold text-white">🧪 iframe 通信测试</span>
|
||||
<button
|
||||
className="text-white/70 hover:text-white"
|
||||
|
|
@ -248,6 +321,31 @@ export function IframeTestPanel() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 场景 5:XClaw 使用状态 */}
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold text-gray-500">
|
||||
⑤ XClaw 使用状态
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 bg-emerald-50 text-xs text-emerald-700 hover:bg-emerald-100"
|
||||
variant="ghost"
|
||||
onClick={() => handleSendXClawUsed(true)}
|
||||
>
|
||||
发送 used=true
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 bg-slate-50 text-xs text-slate-700 hover:bg-slate-100"
|
||||
variant="ghost"
|
||||
onClick={() => handleSendXClawUsed(false)}
|
||||
>
|
||||
发送 used=false
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日志 */}
|
||||
{log.length > 0 && (
|
||||
<div className="rounded-lg bg-gray-900 p-2">
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
||||
|
|
@ -211,9 +212,15 @@ export function InputBox({
|
|||
return;
|
||||
}
|
||||
setIsFocused(false);
|
||||
if (isNewThread) {
|
||||
sendToParent({
|
||||
type: POST_MESSAGE_TYPES.XCLAW_USED,
|
||||
XClawUsed: true,
|
||||
});
|
||||
}
|
||||
onSubmit?.(message);
|
||||
},
|
||||
[onSubmit, onStop, status],
|
||||
[isNewThread, onSubmit, onStop, status],
|
||||
);
|
||||
|
||||
const requestFormSubmit = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
export const POST_MESSAGE_TYPES = {
|
||||
// 全屏切换
|
||||
FULLSCREEN: "fullscreen",
|
||||
// XClaw 使用状态
|
||||
XCLAW_USED: "XClawUsed",
|
||||
// 选择预定义 skill
|
||||
SELECT_SKILL: "selectSkill",
|
||||
// 打开 skill 选择对话框
|
||||
|
|
@ -33,6 +35,11 @@ export interface FullscreenMessage {
|
|||
fullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface XClawUsedMessage {
|
||||
type: typeof POST_MESSAGE_TYPES.XCLAW_USED;
|
||||
XClawUsed: boolean;
|
||||
}
|
||||
|
||||
export interface SelectSkillMessage {
|
||||
type: typeof POST_MESSAGE_TYPES.SELECT_SKILL;
|
||||
skill_id: string;
|
||||
|
|
@ -51,8 +58,13 @@ export interface SelectedSkillMessage {
|
|||
|
||||
// 发送消息的辅助函数
|
||||
export function sendToParent(
|
||||
message: FullscreenMessage | SelectSkillMessage | OpenSkillDialogMessage,
|
||||
message:
|
||||
| FullscreenMessage
|
||||
| XClawUsedMessage
|
||||
| SelectSkillMessage
|
||||
| OpenSkillDialogMessage,
|
||||
): void {
|
||||
console.log("[iframe] sendToParent:", message);
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(message, "*");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
|
||||
import {
|
||||
POST_MESSAGE_TYPES,
|
||||
|
|
@ -23,9 +23,13 @@ interface UseIframeSkillReturn {
|
|||
}
|
||||
|
||||
export function useIframeSkill(): UseIframeSkillReturn {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const skillIdFromQuery = searchParams.get("skill_id");
|
||||
const titleFromQuery = searchParams.get("title");
|
||||
const threadIdFromQuery = searchParams.get("thread_id");
|
||||
const xClawUsedFromQuery = searchParams.get("XClawUsed");
|
||||
const lastThreadIdRef = useRef<string | null>(null);
|
||||
|
||||
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
|
||||
|
||||
|
|
@ -36,6 +40,15 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
|||
}
|
||||
}, [skillIdFromQuery, titleFromQuery]);
|
||||
|
||||
// 0. 监听 query 中 XClawUsed=true 且带 thread_id 时跳转并清理 query
|
||||
useEffect(() => {
|
||||
if (!threadIdFromQuery) return;
|
||||
if (xClawUsedFromQuery !== "true") return;
|
||||
if (lastThreadIdRef.current === threadIdFromQuery) return;
|
||||
lastThreadIdRef.current = threadIdFromQuery;
|
||||
router.replace(`/workspace/chats/${threadIdFromQuery}`);
|
||||
}, [router, threadIdFromQuery, xClawUsedFromQuery]);
|
||||
|
||||
// 2. 监听宿主页 postMessage
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
|
|
|
|||
|
|
@ -452,4 +452,29 @@ p {
|
|||
|
||||
.cm-line {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ͼ4s {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
overflow: hidden;
|
||||
contain: paint;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue