Compare commits
11 Commits
7b53bb0524
...
9cecd24918
| Author | SHA1 | Date |
|---|---|---|
|
|
9cecd24918 | |
|
|
eb45bba7ff | |
|
|
f0d93ab342 | |
|
|
040b107647 | |
|
|
53d383070d | |
|
|
170b5484c9 | |
|
|
d82ac30b93 | |
|
|
a62e65acfe | |
|
|
c52b505354 | |
|
|
d0e0d9e807 | |
|
|
72836322c5 |
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"model_profile": "balanced",
|
||||
"commit_docs": true,
|
||||
"commit_docs": false,
|
||||
"parallelization": true,
|
||||
"search_gitignored": false,
|
||||
"brave_search": false,
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from langchain_core.messages import HumanMessage
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from app.gateway.deps import get_checkpointer, get_run_manager, get_store, get_stream_bridge
|
||||
from deerflow.runtime import (
|
||||
|
|
@ -32,6 +34,17 @@ from deerflow.runtime import (
|
|||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# 预处理提示词的大模型
|
||||
|
||||
PPT_INSUFFICIENT_INFO_FORWARD = "用户想生成ppt,但是没有输入足够多的信息,所以先向用户询问更多信息"
|
||||
PPT_SELECTOR_SYSTEM_PROMPT = """#PPT
|
||||
你是 PPT 技能选择器,严格执行以下流程:
|
||||
用户输入生成 PPT 相关指令后,询问:你需要使用哪个生成 PPT 的技能?可选技能:1. ppt_gen_html(生成 HTML 形式 PPT)2. ppt_gen_reference(根据文档生成 PPT)
|
||||
记住用户最初的 PPT 指令。
|
||||
用户选择技能后,仅输出固定语句,无任何多余内容:
|
||||
选 ppt_gen_html:{user_input},使用 ppt_gen_html 这个 skill 来完成
|
||||
选 ppt_gen_reference:{user_input},使用 ppt_gen_reference 这个 skill 来完成
|
||||
注:“{user_input}” 特指用户最初输入的 PPT 制作指令,非选择回复。"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -94,6 +107,137 @@ def normalize_input(raw_input: dict[str, Any] | None) -> dict[str, Any]:
|
|||
return raw_input
|
||||
|
||||
|
||||
def _extract_text_content(content: Any) -> str:
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
parts: list[str] = []
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
text = item.get("text")
|
||||
if isinstance(text, str) and text.strip():
|
||||
parts.append(text.strip())
|
||||
elif isinstance(item, str) and item.strip():
|
||||
parts.append(item.strip())
|
||||
return "\n".join(parts)
|
||||
return str(content or "")
|
||||
|
||||
|
||||
def _extract_last_human_text(graph_input: dict[str, Any]) -> str:
|
||||
messages = graph_input.get("messages")
|
||||
if not isinstance(messages, list):
|
||||
return ""
|
||||
for msg in reversed(messages):
|
||||
if isinstance(msg, HumanMessage):
|
||||
return _extract_text_content(msg.content).strip()
|
||||
if isinstance(msg, dict):
|
||||
role = str(msg.get("role", msg.get("type", ""))).lower()
|
||||
if role in {"user", "human"}:
|
||||
return _extract_text_content(msg.get("content")).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _is_ppt_request(text: str) -> bool:
|
||||
lowered = text.lower()
|
||||
return any(token in lowered for token in ("ppt", "slides", "powerpoint", "幻灯片", "演示文稿"))
|
||||
|
||||
|
||||
def _heuristic_has_enough_ppt_info(text: str) -> bool:
|
||||
lowered = text.lower()
|
||||
if len(lowered.strip()) < 12:
|
||||
return False
|
||||
|
||||
score = 0
|
||||
if len(lowered) >= 24:
|
||||
score += 1
|
||||
if re.search(r"(关于|主题|topic|题目|on\s+)", lowered):
|
||||
score += 1
|
||||
if re.search(r"(面向|给|用于|目的|audience|for\s+)", lowered):
|
||||
score += 1
|
||||
if re.search(r"(\d+\s*(页|p|slides?)|大纲|目录|章节|结构)", lowered):
|
||||
score += 1
|
||||
if re.search(r"(风格|配色|模板|视觉|语气|style|tone)", lowered):
|
||||
score += 1
|
||||
if re.search(r"(根据|参考|数据|附件|文档|material|reference)", lowered):
|
||||
score += 1
|
||||
return score >= 2
|
||||
|
||||
|
||||
async def _deepseek_ppt_info_check(user_text: str) -> bool:
|
||||
enabled = os.getenv("PPT_PRECHECK_ENABLED", "true").strip().lower()
|
||||
if enabled in {"0", "false", "off", "no"}:
|
||||
return True
|
||||
|
||||
base_url = os.getenv("PPT_PRECHECK_BASE_URL", "").strip()
|
||||
api_key = os.getenv("PPT_PRECHECK_API_KEY", "").strip()
|
||||
model = os.getenv("PPT_PRECHECK_MODEL", "deepseek-chat").strip()
|
||||
timeout_s = float(os.getenv("PPT_PRECHECK_TIMEOUT_SECONDS", "10").strip() or "10")
|
||||
|
||||
if not base_url or not api_key:
|
||||
return _heuristic_has_enough_ppt_info(user_text)
|
||||
|
||||
check_instruction = (
|
||||
"你现在只做“PPT信息是否足够”的判断,不做技能追问。"
|
||||
"判断标准:至少包含主题 + 另一个关键信息(受众/用途/页数或结构/风格/参考资料)。"
|
||||
"仅输出一个词:ENOUGH 或 INSUFFICIENT。"
|
||||
)
|
||||
system_prompt = f"{PPT_SELECTOR_SYSTEM_PROMPT}\n\n{check_instruction}"
|
||||
|
||||
try:
|
||||
client = AsyncOpenAI(base_url=base_url, api_key=api_key, timeout=timeout_s)
|
||||
resp = await client.chat.completions.create(
|
||||
model=model,
|
||||
temperature=0,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_text},
|
||||
],
|
||||
)
|
||||
content = (resp.choices[0].message.content or "").strip().upper()
|
||||
if "INSUFFICIENT" in content:
|
||||
return False
|
||||
if "ENOUGH" in content:
|
||||
return True
|
||||
logger.warning("PPT precheck unexpected output: %r; fallback to heuristic", content)
|
||||
except Exception:
|
||||
logger.warning("PPT precheck via DeepSeek failed; fallback to heuristic", exc_info=True)
|
||||
|
||||
return _heuristic_has_enough_ppt_info(user_text)
|
||||
|
||||
|
||||
def _overwrite_last_human_message(graph_input: dict[str, Any], text: str) -> None:
|
||||
messages = graph_input.get("messages")
|
||||
if not isinstance(messages, list):
|
||||
graph_input["messages"] = [HumanMessage(content=text)]
|
||||
return
|
||||
|
||||
for idx in range(len(messages) - 1, -1, -1):
|
||||
msg = messages[idx]
|
||||
if isinstance(msg, HumanMessage):
|
||||
msg.content = text
|
||||
return
|
||||
if isinstance(msg, dict):
|
||||
role = str(msg.get("role", msg.get("type", ""))).lower()
|
||||
if role in {"user", "human"}:
|
||||
msg["content"] = text
|
||||
return
|
||||
|
||||
messages.append(HumanMessage(content=text))
|
||||
|
||||
|
||||
async def _maybe_apply_ppt_precheck(graph_input: dict[str, Any]) -> None:
|
||||
user_text = _extract_last_human_text(graph_input)
|
||||
if not user_text or not _is_ppt_request(user_text):
|
||||
return
|
||||
|
||||
enough = await _deepseek_ppt_info_check(user_text)
|
||||
if enough:
|
||||
return
|
||||
|
||||
_overwrite_last_human_message(graph_input, PPT_INSUFFICIENT_INFO_FORWARD)
|
||||
logger.info("PPT precheck flagged insufficient info; forwarded clarification instruction")
|
||||
|
||||
|
||||
_DEFAULT_ASSISTANT_ID = "lead_agent"
|
||||
|
||||
|
||||
|
|
@ -282,6 +426,7 @@ async def start_run(
|
|||
|
||||
agent_factory = resolve_agent_factory(body.assistant_id)
|
||||
graph_input = normalize_input(body.input)
|
||||
await _maybe_apply_ppt_precheck(graph_input)
|
||||
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
|
||||
|
||||
if "configurable" in config and isinstance(config["configurable"], dict):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
|
||||
def test_format_sse_basic():
|
||||
|
|
@ -81,6 +84,55 @@ def test_normalize_input_passthrough():
|
|||
assert result == {"custom_key": "value"}
|
||||
|
||||
|
||||
def test_extract_last_human_text_from_human_message():
|
||||
from app.gateway.services import _extract_last_human_text
|
||||
|
||||
graph_input = {
|
||||
"messages": [
|
||||
HumanMessage(content="第一条"),
|
||||
HumanMessage(content=[{"type": "text", "text": "我要做一个产品发布会PPT"}]),
|
||||
]
|
||||
}
|
||||
assert _extract_last_human_text(graph_input) == "我要做一个产品发布会PPT"
|
||||
|
||||
|
||||
def test_is_ppt_request():
|
||||
from app.gateway.services import _is_ppt_request
|
||||
|
||||
assert _is_ppt_request("帮我做个PPT")
|
||||
assert _is_ppt_request("Please generate slides for roadmap")
|
||||
assert not _is_ppt_request("帮我写一段 SQL")
|
||||
|
||||
|
||||
def test_heuristic_has_enough_ppt_info():
|
||||
from app.gateway.services import _heuristic_has_enough_ppt_info
|
||||
|
||||
assert not _heuristic_has_enough_ppt_info("做个ppt")
|
||||
assert _heuristic_has_enough_ppt_info("做一个关于Q2复盘的PPT,面向管理层,10页,简洁风格")
|
||||
|
||||
|
||||
def test_overwrite_last_human_message():
|
||||
from app.gateway.services import _overwrite_last_human_message
|
||||
|
||||
graph_input = {"messages": [HumanMessage(content="请生成PPT")]}
|
||||
_overwrite_last_human_message(graph_input, "用户想生成ppt,但是没有输入足够多的信息,所以先向用户询问更多信息")
|
||||
assert graph_input["messages"][-1].content == "用户想生成ppt,但是没有输入足够多的信息,所以先向用户询问更多信息"
|
||||
|
||||
|
||||
def test_maybe_apply_ppt_precheck_rewrites_when_insufficient():
|
||||
from app.gateway.services import _maybe_apply_ppt_precheck
|
||||
|
||||
graph_input = {"messages": [HumanMessage(content="帮我做个PPT")]}
|
||||
with patch(
|
||||
"app.gateway.services._deepseek_ppt_info_check",
|
||||
new=AsyncMock(return_value=False),
|
||||
):
|
||||
import asyncio
|
||||
|
||||
asyncio.run(_maybe_apply_ppt_precheck(graph_input))
|
||||
assert graph_input["messages"][-1].content == "用户想生成ppt,但是没有输入足够多的信息,所以先向用户询问更多信息"
|
||||
|
||||
|
||||
def test_build_run_config_basic():
|
||||
from app.gateway.services import build_run_config
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"antd": "^6.3.6",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"better-auth": "^1.3",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Ticker } from "@tombcato/smart-ticker";
|
||||
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -22,6 +23,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";
|
||||
|
|
@ -39,8 +41,7 @@ import { textOfMessage } from "@/core/threads/utils";
|
|||
import { env } from "@/env";
|
||||
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
||||
import { Ticker } from "@tombcato/smart-ticker";
|
||||
|
||||
import "@tombcato/smart-ticker/style.css";
|
||||
import motivationSlogans from "./motivation-slogans.json";
|
||||
|
||||
|
|
@ -489,9 +490,7 @@ export default function ChatPage() {
|
|||
/>
|
||||
) : (
|
||||
<div className="relative flex size-full justify-center px-[20px]">
|
||||
<div className="z-30">
|
||||
|
||||
</div>
|
||||
<div className="z-30"></div>
|
||||
{thread.values.artifacts?.length === 0 ? (
|
||||
<ConversationEmptyState
|
||||
icon={<FilesIcon />}
|
||||
|
|
@ -500,8 +499,8 @@ export default function ChatPage() {
|
|||
/>
|
||||
) : (
|
||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
|
||||
<header className="shrink-0 flex justify-between items-center border-b ">
|
||||
<h2 className="text-[14px] h-[58px] leading-[58px] font-bold text-[#333333]">
|
||||
<header className="flex shrink-0 items-center justify-between border-b">
|
||||
<h2 className="h-[58px] text-[14px] leading-[58px] font-bold text-[#333333]">
|
||||
<span>{t.common.artifacts}</span>
|
||||
</h2>
|
||||
<Button
|
||||
|
|
@ -655,7 +654,9 @@ export default function ChatPage() {
|
|||
<DevDialogContent>
|
||||
<DevDialogHeader>
|
||||
<DevDialogTitle>
|
||||
⚠️ {selectedSkillError?.title ?? t.chatPage.selectedSkillLoadFailed}
|
||||
⚠️{" "}
|
||||
{selectedSkillError?.title ??
|
||||
t.chatPage.selectedSkillLoadFailed}
|
||||
</DevDialogTitle>
|
||||
</DevDialogHeader>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const Message = ({
|
|||
"group flex w-full flex-col gap-2",
|
||||
from === "user"
|
||||
? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6")
|
||||
: "is-assistant bg-white rounded-[10px] p-4",
|
||||
: "is-assistant rounded-[10px] bg-white p-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1064,7 +1064,7 @@ export const PromptInputTools = ({
|
|||
className,
|
||||
...props
|
||||
}: PromptInputToolsProps) => (
|
||||
<div className={cn("flex items-center gap-1", className)} {...props} />
|
||||
<div className={cn("flex items-center h-full gap-1", className)} {...props} />
|
||||
);
|
||||
|
||||
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ export const ReasoningContent = memo(
|
|||
{...props}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<div className="whitespace-pre-wrap break-words">{children}</div>
|
||||
<div className="break-words whitespace-pre-wrap">{children}</div>
|
||||
) : (
|
||||
<Streamdown
|
||||
isAnimating={false}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import * as React from "react";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
|
|
@ -17,7 +17,7 @@ function ContextMenuTrigger({
|
|||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
|
|
@ -25,7 +25,7 @@ function ContextMenuGroup({
|
|||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
|
|
@ -33,13 +33,13 @@ function ContextMenuPortal({
|
|||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
|
|
@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
|
|||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
|
|
@ -59,22 +59,22 @@ function ContextMenuSubTrigger({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
"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 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,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
|
|
@ -85,12 +85,12 @@ function ContextMenuSubContent({
|
|||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
"bg-popover text-popover-foreground 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
|
|
@ -102,13 +102,13 @@ function ContextMenuContent({
|
|||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-0 text-popover-foreground shadow-md 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
"bg-popover text-popover-foreground 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=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-0 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
|
|
@ -117,8 +117,8 @@ function ContextMenuItem({
|
|||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
|
|
@ -126,12 +126,12 @@ function ContextMenuItem({
|
|||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive! 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,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
|
|
@ -144,8 +144,8 @@ function ContextMenuCheckboxItem({
|
|||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
"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,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
|
|
@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
|
|||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
|
|
@ -169,8 +169,8 @@ function ContextMenuRadioItem({
|
|||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
"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,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -181,7 +181,7 @@ function ContextMenuRadioItem({
|
|||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
|
|
@ -189,19 +189,19 @@ function ContextMenuLabel({
|
|||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8",
|
||||
className
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
|
|
@ -211,10 +211,10 @@ function ContextMenuSeparator({
|
|||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
|
|
@ -225,12 +225,12 @@ function ContextMenuShortcut({
|
|||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -249,4 +249,4 @@ export {
|
|||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,7 +81,9 @@ export function DropdownSelector<T extends string>({
|
|||
}
|
||||
>
|
||||
<span className="flex w-full items-center justify-center gap-1">
|
||||
{truncateMiddle(selectedOption?.label ?? value, 30)}
|
||||
{/* {truncateMiddle(selectedOption?.label ?? value, 20)} */}
|
||||
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
|
||||
|
||||
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
@ -98,7 +100,8 @@ export function DropdownSelector<T extends string>({
|
|||
value={option.value}
|
||||
title={option.label}
|
||||
>
|
||||
{truncateMiddle(option.label)}
|
||||
{/* {truncateMiddle(option.label,50)} */}
|
||||
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||
"text-muted-foreground flex h-[58px] cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
|
|
@ -46,9 +46,9 @@ const inputGroupAddonVariants = cva(
|
|||
"inline-end":
|
||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||
"block-start":
|
||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||
"order-first w-full justify-start px-3 pt-5 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||
"block-end":
|
||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||
"order-last w-full justify-start px-3 py-0 pb-5 group-has-[>input]/input-group:pb-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import { cn } from "@/lib/utils";
|
|||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
hideScrollbar = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
|
||||
hideScrollbar?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
|
|
@ -22,8 +25,8 @@ function ScrollArea({
|
|||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
<ScrollBar hidden={hideScrollbar} />
|
||||
<ScrollAreaPrimitive.Corner hidden={hideScrollbar} />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
|
|
@ -20,8 +20,8 @@ function Slider({
|
|||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max]
|
||||
)
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
|
|
@ -32,20 +32,20 @@ function Slider({
|
|||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
|
|
@ -53,11 +53,11 @@ function Slider({
|
|||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
export { Slider };
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import ExcelJS from "exceljs";
|
||||
import JSZip from "jszip";
|
||||
import {
|
||||
DownloadIcon,
|
||||
|
|
@ -17,7 +18,6 @@ import {
|
|||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Streamdown } from "streamdown";
|
||||
import ExcelJS from "exceljs";
|
||||
|
||||
import {
|
||||
Artifact,
|
||||
|
|
@ -73,13 +73,11 @@ let revoGridLoaderPromise: Promise<void> | null = null;
|
|||
function ensureRevoGridDefined() {
|
||||
if (typeof window === "undefined") return Promise.resolve();
|
||||
if (window.customElements.get("revo-grid")) return Promise.resolve();
|
||||
if (!revoGridLoaderPromise) {
|
||||
revoGridLoaderPromise = import("@revolist/revogrid/loader").then(
|
||||
revoGridLoaderPromise ??= import("@revolist/revogrid/loader").then(
|
||||
({ defineCustomElements }) => {
|
||||
defineCustomElements(window);
|
||||
},
|
||||
);
|
||||
}
|
||||
return revoGridLoaderPromise;
|
||||
}
|
||||
|
||||
|
|
@ -99,18 +97,45 @@ function toGridCellText(cell: ExcelJS.Cell): string {
|
|||
const value = cell.value;
|
||||
if (value == null) return "";
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "bigint"
|
||||
) {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
if ("result" in value && value.result != null) {
|
||||
return String(value.result);
|
||||
const result = value.result;
|
||||
if (
|
||||
typeof result === "string" ||
|
||||
typeof result === "number" ||
|
||||
typeof result === "boolean" ||
|
||||
typeof result === "bigint"
|
||||
) {
|
||||
return String(result);
|
||||
}
|
||||
}
|
||||
if ("text" in value && value.text) {
|
||||
return String(value.text);
|
||||
const text = value.text;
|
||||
if (
|
||||
typeof text === "string" ||
|
||||
typeof text === "number" ||
|
||||
typeof text === "boolean" ||
|
||||
typeof text === "bigint"
|
||||
) {
|
||||
return String(text);
|
||||
}
|
||||
}
|
||||
if ("hyperlink" in value && value.hyperlink) {
|
||||
return String(value.hyperlink);
|
||||
const hyperlink = value.hyperlink;
|
||||
if (typeof hyperlink === "string") {
|
||||
return hyperlink;
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
|
||||
|
|
@ -398,8 +423,8 @@ export function ArtifactFileDetail({
|
|||
className,
|
||||
)}
|
||||
>
|
||||
<ArtifactHeader className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-3 flex min-w-0 items-center justify-start gap-2 overflow-hidden">
|
||||
<ArtifactHeader className="grid grid-cols-24">
|
||||
<div className="col-span-7 flex min-w-0 items-center justify-start gap-2 overflow-hidden">
|
||||
{previewable && (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
|
|
@ -464,11 +489,11 @@ export function ArtifactFileDetail({
|
|||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="col-span-6 flex min-w-0 items-center justify-center px-1">
|
||||
<div className="col-span-10 flex min-w-0 items-center justify-center px-1">
|
||||
<ArtifactTitle>
|
||||
{isWriteFile ? (
|
||||
<div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
|
||||
{truncateMiddle(getFileName(filepath), 50)}
|
||||
{truncateMiddle(getFileName(filepath), 20)}
|
||||
</div>
|
||||
) : (
|
||||
<DropdownSelector
|
||||
|
|
@ -479,7 +504,7 @@ export function ArtifactFileDetail({
|
|||
)}
|
||||
</ArtifactTitle>
|
||||
</div>
|
||||
<div className="col-span-3 flex min-w-0 items-center justify-end overflow-hidden">
|
||||
<div className="col-span-7 flex min-w-0 items-center justify-end overflow-hidden">
|
||||
<ArtifactActions>
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
|
|
@ -1702,12 +1727,18 @@ export const ArtifactZoomSelector = ({
|
|||
</svg>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" sideOffset={8} className="w-52 p-[20px] ">
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-52 p-[20px]"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{ZOOM_LEVELS[0]}%
|
||||
</span>
|
||||
<span className="text-foreground text-xs font-medium">{value}%</span>
|
||||
<span className="text-foreground text-xs font-medium">
|
||||
{value}%
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,9 @@ const ChatBox: React.FC<{
|
|||
) : (
|
||||
<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>
|
||||
<h2 className="text-lg font-medium">
|
||||
{t.common.artifacts}
|
||||
</h2>
|
||||
</header>
|
||||
<main className="min-h-0 grow">
|
||||
<ArtifactFileList
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { ChatStatus } from "ai";
|
||||
import { Tour } from "antd";
|
||||
import {
|
||||
CheckIcon,
|
||||
GraduationCapIcon,
|
||||
|
|
@ -15,8 +14,11 @@ import {
|
|||
XIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
|
|
@ -25,7 +27,9 @@ import {
|
|||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
type ComponentProps,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
PromptInput,
|
||||
|
|
@ -71,15 +75,14 @@ import { useI18n } from "@/core/i18n/hooks";
|
|||
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import {
|
||||
MENTION_REFERENCE_EVENT,
|
||||
type MentionReferenceEventDetail,
|
||||
} from "@/core/threads/reference-events";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { useUploadedFiles } from "@/core/uploads/hooks";
|
||||
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
ModelSelector,
|
||||
|
|
@ -91,13 +94,33 @@ import {
|
|||
ModelSelectorTrigger,
|
||||
} from "../ai-elements/model-selector";
|
||||
import { Suggestion, Suggestions } from "../ai-elements/suggestion";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
|
||||
import { useThread } from "./messages/context";
|
||||
import { ModeHoverGuide } from "./mode-hover-guide";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { useThread } from "./messages/context";
|
||||
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
|
||||
|
||||
const MAX_REFERENCES_PER_MESSAGE = 10;
|
||||
const INPUT_TOOLS_TOUR_SEEN_KEY = "workspace.input_tools_tour_seen.v1";
|
||||
|
||||
type WorkspaceToolButtonProps = ComponentProps<typeof PromptInputButton>;
|
||||
|
||||
function WorkspaceToolButton({
|
||||
className,
|
||||
...props
|
||||
}: WorkspaceToolButtonProps) {
|
||||
return (
|
||||
<PromptInputButton
|
||||
className={cn(
|
||||
// border border-[rgba(0,0,0,0.08)]
|
||||
"group h-full p-[10px]! rounded-[10px] hover:bg-[#EAE2F5] hover:text-[#8E47F0]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type MentionCandidate = {
|
||||
key: string;
|
||||
|
|
@ -214,6 +237,10 @@ export function InputBox({
|
|||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mentionTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const historyButtonTourRef = useRef<HTMLDivElement | null>(null);
|
||||
const attachmentsButtonTourRef = useRef<HTMLDivElement | null>(null);
|
||||
const skillButtonTourRef = useRef<HTMLDivElement | null>(null);
|
||||
const suggestionListTourRef = useRef<HTMLDivElement | null>(null);
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||
|
|
@ -230,11 +257,97 @@ export function InputBox({
|
|||
start: number;
|
||||
end: number;
|
||||
} | null>(null);
|
||||
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
|
||||
const [isInputToolsTourReady, setIsInputToolsTourReady] = useState(false);
|
||||
const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps);
|
||||
|
||||
// isNewThread 时禁用收缩,始终保持展开(除非已提交消息)
|
||||
const effectiveIsFocused =
|
||||
((showWelcomeStyle ?? false) && !hasSubmitted) || isFocused;
|
||||
const shouldShowSuggestionList =
|
||||
showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill";
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWelcomeStyle || hasSubmitted) {
|
||||
setIsInputToolsTourReady(false);
|
||||
return;
|
||||
}
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
setIsInputToolsTourReady(
|
||||
Boolean(
|
||||
historyButtonTourRef.current &&
|
||||
attachmentsButtonTourRef.current &&
|
||||
skillButtonTourRef.current &&
|
||||
(!shouldShowSuggestionList || suggestionListTourRef.current),
|
||||
),
|
||||
);
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frameId);
|
||||
}, [
|
||||
showWelcomeStyle,
|
||||
hasSubmitted,
|
||||
shouldShowSuggestionList,
|
||||
iframeSkill.isBootstrapping,
|
||||
iframeSkill.selectedSkills.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWelcomeStyle || hasSubmitted || !isInputToolsTourReady) {
|
||||
setIsInputToolsTourOpen(false);
|
||||
return;
|
||||
}
|
||||
const hasSeenTour = window.localStorage.getItem(INPUT_TOOLS_TOUR_SEEN_KEY);
|
||||
if (!hasSeenTour) {
|
||||
setIsInputToolsTourOpen(true);
|
||||
}
|
||||
}, [showWelcomeStyle, hasSubmitted, isInputToolsTourReady]);
|
||||
|
||||
const closeInputToolsTour = useCallback(() => {
|
||||
window.localStorage.setItem(INPUT_TOOLS_TOUR_SEEN_KEY, "1");
|
||||
setIsInputToolsTourOpen(false);
|
||||
}, []);
|
||||
|
||||
const inputToolsTourSteps = useMemo(() => {
|
||||
const baseSteps = [
|
||||
{
|
||||
title: "查看历史",
|
||||
description: "点击这里,可以查看历史会话与文档。",
|
||||
target: () => historyButtonTourRef.current ?? document.body,
|
||||
},
|
||||
{
|
||||
title: "上传附件",
|
||||
description: "点击这里,上传参考文档或拟处理的文档。",
|
||||
target: () => attachmentsButtonTourRef.current ?? document.body,
|
||||
},
|
||||
{
|
||||
title: "选择 Skill",
|
||||
description: (
|
||||
<>
|
||||
点击这里,从“我的skill”中选择要使用的skill。
|
||||
<br />
|
||||
在广场中选择skill,在详情页选择“去使用”,也可选中skill。
|
||||
</>
|
||||
),
|
||||
target: () => skillButtonTourRef.current ?? document.body,
|
||||
},
|
||||
...(shouldShowSuggestionList
|
||||
? [
|
||||
{
|
||||
title: "试试我吧",
|
||||
target: () => suggestionListTourRef.current ?? document.body,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return baseSteps.map((step, index) => ({
|
||||
...step,
|
||||
prevButtonProps: { children: "上一步" },
|
||||
nextButtonProps: {
|
||||
children: index === baseSteps.length - 1 ? "完成" : "下一步",
|
||||
},
|
||||
}));
|
||||
}, [shouldShowSuggestionList]);
|
||||
|
||||
// 点击外部区域时收起输入框
|
||||
useEffect(() => {
|
||||
|
|
@ -372,7 +485,14 @@ export function InputBox({
|
|||
});
|
||||
setReferences([]);
|
||||
},
|
||||
[showWelcomeStyle, onSubmit, onStop, references, status, iframeSkill.selectedSkills],
|
||||
[
|
||||
showWelcomeStyle,
|
||||
onSubmit,
|
||||
onStop,
|
||||
references,
|
||||
status,
|
||||
iframeSkill.selectedSkills,
|
||||
],
|
||||
);
|
||||
|
||||
const requestFormSubmit = useCallback(() => {
|
||||
|
|
@ -380,7 +500,8 @@ export function InputBox({
|
|||
form?.requestSubmit();
|
||||
}, []);
|
||||
|
||||
const addMentionReference = useCallback((reference: PromptInputReference) => {
|
||||
const addMentionReference = useCallback(
|
||||
(reference: PromptInputReference) => {
|
||||
setReferences((prev) => {
|
||||
const exists = prev.some(
|
||||
(item) =>
|
||||
|
|
@ -397,7 +518,9 @@ export function InputBox({
|
|||
}
|
||||
return prev.concat(reference);
|
||||
});
|
||||
}, [t.inputBox.maxReferencesReached]);
|
||||
},
|
||||
[t.inputBox.maxReferencesReached],
|
||||
);
|
||||
|
||||
const selectMentionCandidate = useCallback(
|
||||
(candidate: MentionCandidate) => {
|
||||
|
|
@ -432,7 +555,7 @@ export function InputBox({
|
|||
useEffect(() => {
|
||||
const onMentionReference = (event: Event) => {
|
||||
const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail;
|
||||
if (!detail || detail.threadId !== threadIdFromProps) {
|
||||
if (detail?.threadId !== threadIdFromProps) {
|
||||
return;
|
||||
}
|
||||
addMentionReference({
|
||||
|
|
@ -492,12 +615,13 @@ export function InputBox({
|
|||
}
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setActiveMentionIndex((prev) =>
|
||||
(prev + 1) % filteredMentionCandidates.length,
|
||||
setActiveMentionIndex(
|
||||
(prev) => (prev + 1) % filteredMentionCandidates.length,
|
||||
);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setActiveMentionIndex((prev) =>
|
||||
setActiveMentionIndex(
|
||||
(prev) =>
|
||||
(prev - 1 + filteredMentionCandidates.length) %
|
||||
filteredMentionCandidates.length,
|
||||
);
|
||||
|
|
@ -603,6 +727,21 @@ export function InputBox({
|
|||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
<Tour
|
||||
open={isInputToolsTourOpen}
|
||||
onClose={closeInputToolsTour}
|
||||
onFinish={closeInputToolsTour}
|
||||
gap={
|
||||
{ offset: 4 , radius: 2 }
|
||||
}
|
||||
mask={{
|
||||
style: {
|
||||
boxShadow: 'inset 0 0 15px #333',
|
||||
},
|
||||
color: 'rgba(255,255,255, .8)',
|
||||
}}
|
||||
steps={inputToolsTourSteps}
|
||||
/>
|
||||
<AttachmentPreviewBar
|
||||
references={references}
|
||||
threadId={threadId}
|
||||
|
|
@ -658,7 +797,11 @@ export function InputBox({
|
|||
!effectiveIsFocused && "h-[80px] py-0 leading-20",
|
||||
)}
|
||||
disabled={isInputDisabled}
|
||||
placeholder={t.inputBox.placeholder}
|
||||
placeholder={
|
||||
showWelcomeStyle
|
||||
? t.inputBox.welcomePlaceholder
|
||||
: t.inputBox.chatPlaceholder
|
||||
}
|
||||
autoFocus={autoFocus}
|
||||
defaultValue={initialValue}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
|
|
@ -688,7 +831,7 @@ export function InputBox({
|
|||
align="start"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
className="w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] max-h-[400px] overflow-y-visible p-[20px]"
|
||||
className="max-h-[400px] w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] overflow-y-hidden p-[20px]"
|
||||
data-testid="mention-candidate-panel"
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -699,8 +842,9 @@ export function InputBox({
|
|||
{t.inputBox.addReference}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
||||
<DropdownMenuGroup className="flex pt-[20px] px-0 max-h-[480px] flex-col gap-[10px] overflow-y-auto">
|
||||
{filteredMentionCandidates.slice(0, 20).map((candidate, index) => {
|
||||
<DropdownMenuGroup className="flex max-h-[480px] flex-col gap-[10px] px-0 pt-[20px]">
|
||||
<ScrollArea className="h-[480px]" data-state="hidden">
|
||||
{filteredMentionCandidates.map((candidate, index) => {
|
||||
const detail = [candidate.typeLabel, candidate.pathTail]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
|
@ -711,7 +855,9 @@ export function InputBox({
|
|||
"flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left",
|
||||
index === activeMentionIndex && "bg-accent",
|
||||
)}
|
||||
data-active={index === activeMentionIndex ? "true" : "false"}
|
||||
data-active={
|
||||
index === activeMentionIndex ? "true" : "false"
|
||||
}
|
||||
data-candidate-key={candidate.key}
|
||||
data-testid="mention-candidate-item"
|
||||
aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`}
|
||||
|
|
@ -744,6 +890,7 @@ export function InputBox({
|
|||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -764,7 +911,7 @@ export function InputBox({
|
|||
"pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0",
|
||||
)}
|
||||
>
|
||||
<PromptInputTools className="min-w-0 flex-1 gap-[20px]">
|
||||
<PromptInputTools className="min-w-0 w-full overflow-hidden gap-[20px]">
|
||||
{/* TODO: Add more connectors here
|
||||
<PromptInputActionMenu>
|
||||
<PromptInputActionMenuTrigger className="px-2!" />
|
||||
|
|
@ -774,19 +921,27 @@ export function InputBox({
|
|||
/>
|
||||
</PromptInputActionMenuContent>
|
||||
</PromptInputActionMenu> */}
|
||||
{showWelcomeStyle && <HistoryButton
|
||||
className="px-2!"
|
||||
{showWelcomeStyle && (
|
||||
<div ref={historyButtonTourRef} className="shrink-0 h-full">
|
||||
<HistoryButton
|
||||
router={router}
|
||||
threadId={threadIdFromProps}
|
||||
/>}
|
||||
<AddAttachmentsButton className="px-2!" />
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div ref={attachmentsButtonTourRef} className="shrink-0 h-full">
|
||||
<AddAttachmentsButton />
|
||||
</div>
|
||||
<div className="min-w-0 grow basis-0 h-full">
|
||||
<IframeSkillDialogButton
|
||||
className="px-2!"
|
||||
skillButtonRef={skillButtonTourRef}
|
||||
selectedSkills={iframeSkill.selectedSkills}
|
||||
isBootstrapping={iframeSkill.isBootstrapping}
|
||||
openSkillDialog={iframeSkill.openSkillDialog}
|
||||
clearSkill={iframeSkill.clearSkill}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="h-[40px] w-[140px] shrink-0" aria-hidden="true" /> */}
|
||||
|
||||
{/* 参考 kexue 版本隐藏运行模式切换按钮 */}
|
||||
</PromptInputTools>
|
||||
|
|
@ -823,7 +978,7 @@ export function InputBox({
|
|||
</ModelSelector> */}
|
||||
<PromptInputTools>
|
||||
{/* 占位符 */}
|
||||
<div className="w-[150px]"></div>
|
||||
<div className="w-[150px] h-[40px]"></div>
|
||||
</PromptInputTools>
|
||||
</PromptInputFooter>
|
||||
<PromptInputSubmit
|
||||
|
|
@ -834,10 +989,9 @@ export function InputBox({
|
|||
/>
|
||||
</PromptInput>
|
||||
|
||||
{showWelcomeStyle &&
|
||||
!hasSubmitted &&
|
||||
searchParams.get("mode") !== "skill" && (
|
||||
{shouldShowSuggestionList && (
|
||||
<SuggestionListContainer
|
||||
ref={suggestionListTourRef}
|
||||
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
|
||||
isBootstrapping={iframeSkill.isBootstrapping}
|
||||
/>
|
||||
|
|
@ -904,25 +1058,29 @@ export function InputBox({
|
|||
}
|
||||
|
||||
// SuggestionList 容器
|
||||
function SuggestionListContainer({
|
||||
bootstrapAndLockSkills,
|
||||
isBootstrapping,
|
||||
}: {
|
||||
const SuggestionListContainer = forwardRef<HTMLDivElement, {
|
||||
bootstrapAndLockSkills: (params: {
|
||||
selectedSkills: SelectedSkillPayloadItem[];
|
||||
title: string;
|
||||
}) => Promise<boolean>;
|
||||
isBootstrapping: boolean;
|
||||
}) {
|
||||
}>(
|
||||
function SuggestionListContainer(
|
||||
{ bootstrapAndLockSkills, isBootstrapping },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4">
|
||||
<div ref={ref} className="w-fit">
|
||||
<SuggestionList
|
||||
bootstrapAndLockSkills={bootstrapAndLockSkills}
|
||||
isBootstrapping={isBootstrapping}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 快速选择skillbutton
|
||||
function SuggestionList({
|
||||
|
|
@ -1024,8 +1182,8 @@ function AddAttachmentsButton({ className }: { className?: string }) {
|
|||
const attachments = usePromptInputAttachments();
|
||||
return (
|
||||
<Tooltip content={t.inputBox.addAttachments}>
|
||||
<PromptInputButton
|
||||
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||
<WorkspaceToolButton
|
||||
className={className}
|
||||
onClick={() => attachments.openFileDialog()}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -1045,7 +1203,7 @@ function AddAttachmentsButton({ className }: { className?: string }) {
|
|||
stroke="#150033"
|
||||
/>
|
||||
</svg>
|
||||
</PromptInputButton>
|
||||
</WorkspaceToolButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -1062,8 +1220,8 @@ function HistoryButton({
|
|||
const { t } = useI18n();
|
||||
return (
|
||||
<Tooltip content={t.inputBox.history}>
|
||||
<PromptInputButton
|
||||
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||
<WorkspaceToolButton
|
||||
className={className}
|
||||
onClick={() =>
|
||||
router.replace(`/workspace/chats/${threadId}?is_chatting=true`)
|
||||
}
|
||||
|
|
@ -1089,20 +1247,21 @@ function HistoryButton({
|
|||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
</PromptInputButton>
|
||||
</WorkspaceToolButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
// 启动iframeSkillDialog
|
||||
function IframeSkillDialogButton({
|
||||
className,
|
||||
skillButtonRef,
|
||||
selectedSkills,
|
||||
isBootstrapping,
|
||||
openSkillDialog,
|
||||
clearSkill,
|
||||
}: {
|
||||
className?: string;
|
||||
skillButtonRef?: RefObject<HTMLDivElement | null>;
|
||||
selectedSkills: Array<{ skill_id: string; title: string }>;
|
||||
isBootstrapping: boolean;
|
||||
openSkillDialog: () => void;
|
||||
|
|
@ -1111,10 +1270,11 @@ function IframeSkillDialogButton({
|
|||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div className="flex min-w-0 w-full items-center h-full gap-2">
|
||||
<Tooltip content={t.inputBox.selectSkill}>
|
||||
<PromptInputButton
|
||||
className={cn("group shrink-0 px-2! hover:bg-[#EAE2F5]", className)}
|
||||
<div ref={skillButtonRef} className="shrink-0">
|
||||
<WorkspaceToolButton
|
||||
className={cn("shrink-0", className)}
|
||||
onClick={openSkillDialog}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -1128,7 +1288,8 @@ function IframeSkillDialogButton({
|
|||
stroke="#150033"
|
||||
/>
|
||||
</svg>
|
||||
</PromptInputButton>
|
||||
</WorkspaceToolButton>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isBootstrapping ? (
|
||||
<Tag className="bg-background text-muted-foreground gap-2 border">
|
||||
|
|
@ -1138,7 +1299,7 @@ function IframeSkillDialogButton({
|
|||
) : null}
|
||||
{!isBootstrapping && selectedSkills.length > 0 ? (
|
||||
<div
|
||||
className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="flex min-w-0 grow basis-0 items-center gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
onWheel={(event) => {
|
||||
if (event.deltaY === 0) return;
|
||||
event.currentTarget.scrollLeft += event.deltaY;
|
||||
|
|
@ -1198,7 +1359,10 @@ function AttachmentPreviewBar({
|
|||
</PromptInputAttachments>
|
||||
)}
|
||||
{hasReferences && (
|
||||
<div className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2" data-testid="reference-inline-preview">
|
||||
<div
|
||||
className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2"
|
||||
data-testid="reference-inline-preview"
|
||||
>
|
||||
{references.map((reference) => {
|
||||
const referenceUrl =
|
||||
threadId && reference.path
|
||||
|
|
@ -1208,7 +1372,7 @@ function AttachmentPreviewBar({
|
|||
})
|
||||
: null;
|
||||
const filename = reference.filename ?? "reference";
|
||||
const imageMatch = filename.match(/\.(png|jpe?g|gif|webp|bmp|svg)$/i);
|
||||
const imageMatch = /\.(png|jpe?g|gif|webp|bmp|svg)$/i.exec(filename);
|
||||
const extension = imageMatch?.[1]?.toLowerCase();
|
||||
const mediaType = extension
|
||||
? extension === "jpg"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { AnchorHTMLAttributes } from "react";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState, type MouseEvent } from "react";
|
||||
import type {
|
||||
AnchorHTMLAttributes,
|
||||
ComponentPropsWithoutRef,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
MessageResponse,
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, copyToClipboard } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
|
||||
|
|
@ -25,6 +31,97 @@ export type MarkdownContentProps = {
|
|||
components?: MessageResponseProps["components"];
|
||||
};
|
||||
|
||||
type TableData = {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
};
|
||||
|
||||
function parseTableData(table: HTMLTableElement): TableData {
|
||||
const headers = Array.from(table.querySelectorAll("thead th")).map((cell) =>
|
||||
(cell.textContent ?? "").trim(),
|
||||
);
|
||||
const rows = Array.from(table.querySelectorAll("tbody tr")).map((row) =>
|
||||
Array.from(row.querySelectorAll("td")).map((cell) =>
|
||||
(cell.textContent ?? "").trim(),
|
||||
),
|
||||
);
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
function toMarkdownTable(data: TableData): string {
|
||||
if (data.headers.length === 0) return "";
|
||||
const headerLine = `| ${data.headers.join(" | ")} |`;
|
||||
const dividerLine = `| ${data.headers.map(() => "---").join(" | ")} |`;
|
||||
const rowLines = data.rows.map((row) => `| ${row.join(" | ")} |`);
|
||||
return [headerLine, dividerLine, ...rowLines].join("\n");
|
||||
}
|
||||
|
||||
function MarkdownTable({
|
||||
className,
|
||||
children,
|
||||
isLoading,
|
||||
copyLabel,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<"table"> & {
|
||||
isLoading: boolean;
|
||||
copyLabel: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
async (event: MouseEvent<HTMLButtonElement>) => {
|
||||
const wrapper = event.currentTarget.closest(
|
||||
'[data-streamdown="table-wrapper"]',
|
||||
);
|
||||
const table = wrapper?.querySelector("table");
|
||||
if (!(table instanceof HTMLTableElement)) return;
|
||||
|
||||
const markdown = toMarkdownTable(parseTableData(table));
|
||||
if (!markdown) return;
|
||||
|
||||
try {
|
||||
await copyToClipboard(markdown);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="my-4 flex flex-col space-y-2"
|
||||
data-streamdown="table-wrapper"
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
className="text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
onClick={handleCopy}
|
||||
title={copyLabel}
|
||||
type="button"
|
||||
>
|
||||
{copied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className={cn(
|
||||
"border-border w-full border-collapse border",
|
||||
className,
|
||||
)}
|
||||
data-streamdown="table"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders markdown content. */
|
||||
export function MarkdownContent({
|
||||
content,
|
||||
|
|
@ -34,6 +131,8 @@ export function MarkdownContent({
|
|||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||
components: componentsFromProps,
|
||||
}: MarkdownContentProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
|
|
@ -58,9 +157,23 @@ export function MarkdownContent({
|
|||
/>
|
||||
);
|
||||
},
|
||||
table: ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<"table"> & { children?: ReactNode }) => (
|
||||
<MarkdownTable
|
||||
className={className}
|
||||
copyLabel={t.clipboard.copyToClipboard}
|
||||
isLoading={isLoading}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MarkdownTable>
|
||||
),
|
||||
...componentsFromProps,
|
||||
};
|
||||
}, [componentsFromProps]);
|
||||
}, [componentsFromProps, isLoading, t.clipboard.copyToClipboard]);
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
|
|
@ -68,6 +181,7 @@ export function MarkdownContent({
|
|||
<MessageResponse
|
||||
className={className}
|
||||
isAnimating={isLoading}
|
||||
controls={{ table: false }}
|
||||
parseIncompleteMarkdown={!isLoading}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import { Tooltip } from "../tooltip";
|
|||
|
||||
import { MarkdownContent } from "./markdown-content";
|
||||
|
||||
|
||||
export function MessageGroup({
|
||||
className,
|
||||
messages,
|
||||
|
|
@ -87,11 +86,7 @@ export function MessageGroup({
|
|||
const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
|
||||
const thinkingComponents = useMemo(
|
||||
() => ({
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"code">) => {
|
||||
code: ({ className, children, ...props }: ComponentProps<"code">) => {
|
||||
const isBlock =
|
||||
typeof className === "string" && className.includes("language-");
|
||||
if (!isBlock) {
|
||||
|
|
@ -126,7 +121,7 @@ export function MessageGroup({
|
|||
<Button
|
||||
key="above"
|
||||
// 等宋
|
||||
className="w-full items-start justify-start text-left h-auto! py-4"
|
||||
className="h-auto! w-full items-start justify-start py-4 text-left"
|
||||
variant="ghost"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ import {
|
|||
stripUploadedFilesTag,
|
||||
type FileInMessage,
|
||||
} from "@/core/messages/utils";
|
||||
import { dispatchMentionReference } from "@/core/threads/reference-events";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { materializeSkillYaml } from "@/core/skills";
|
||||
import { humanMessagePlugins } from "@/core/streamdown";
|
||||
import { dispatchMentionReference } from "@/core/threads/reference-events";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CopyButton } from "../copy-button";
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
|||
) : (
|
||||
<div className="text-primary ml-2 cursor-default font-serif">
|
||||
{/* TODO: 测试标识 */}
|
||||
XClaw <span className="text-sm text-[#000000c5]">v3.2.7</span>
|
||||
XClaw <span className="text-sm text-[#000000c5]">v3.2.8</span>
|
||||
</div>
|
||||
)}
|
||||
<SidebarTrigger />
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ export const enUS: Translations = {
|
|||
// Input Box
|
||||
inputBox: {
|
||||
placeholder: "How can I assist you today?",
|
||||
welcomePlaceholder:
|
||||
"Start chatting directly, or describe your task and pick a skill for professional execution.",
|
||||
chatPlaceholder: "Type “@” to reference files.",
|
||||
createSkillPrompt:
|
||||
"We're going to build a new skill step by step with `skill-creator`. To start, what do you want this skill to do?",
|
||||
sendMessagePrice:
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export interface Translations {
|
|||
inputBox: {
|
||||
sendMessagePrice: string;
|
||||
placeholder: string;
|
||||
welcomePlaceholder: string;
|
||||
chatPlaceholder: string;
|
||||
createSkillPrompt: string;
|
||||
addAttachments: string;
|
||||
history: string;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ export const zhCN: Translations = {
|
|||
|
||||
// Input Box
|
||||
inputBox: {
|
||||
placeholder: "可直接对话; 或输入需求并选择skill,完成专业任务;“@”可引用文件",
|
||||
placeholder: "可直接对话; 或输入需求并选择skill,完成专业任务;",
|
||||
welcomePlaceholder: "可直接对话; 或输入需求并选择skill,完成专业任务。",
|
||||
chatPlaceholder: "“@”可引用文件。",
|
||||
createSkillPrompt:
|
||||
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",
|
||||
sendMessagePrice:
|
||||
|
|
@ -259,7 +261,8 @@ export const zhCN: Translations = {
|
|||
noArtifactSelectedTitle: "未选择生成文件",
|
||||
noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
|
||||
exitDialogTitle: "提示",
|
||||
exitDialogDescription: "历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
|
||||
exitDialogDescription:
|
||||
"历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
|
||||
exitDialogConfirm: "确定",
|
||||
selectedSkillLoadFailed: "技能加载失败",
|
||||
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ import type { UploadedFileInfo } from "../uploads";
|
|||
import { listUploadedFiles, uploadFiles } from "../uploads";
|
||||
import type { UploadTarget } from "../uploads/api";
|
||||
|
||||
import { buildFilesForSubmit } from "./submit-files";
|
||||
import { buildPriorityHintText, composeSubmitText } from "./priority-hint";
|
||||
import { buildFilesForSubmit } from "./submit-files";
|
||||
import type {
|
||||
AgentThread,
|
||||
AgentThreadContext,
|
||||
|
|
@ -268,8 +268,7 @@ export function useThreadStream({
|
|||
const now = Date.now();
|
||||
const lastToast = lastErrorToastRef.current;
|
||||
if (
|
||||
lastToast &&
|
||||
lastToast.message === message &&
|
||||
lastToast?.message === message &&
|
||||
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
|
||||
) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
POST_MESSAGE_TYPES,
|
||||
RECEIVE_MESSAGE_TYPES,
|
||||
|
|
@ -10,7 +11,6 @@ import {
|
|||
type SelectedSkillPayloadItem,
|
||||
sendToParent,
|
||||
} from "@/core/iframe-messages";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||
|
||||
// Skill 数据类型
|
||||
|
|
@ -39,8 +39,20 @@ function parseStoredSkills(raw: string | null): SkillData[] {
|
|||
.map((item) => {
|
||||
if (typeof item !== "object" || item === null) return null;
|
||||
const record = item as Record<string, unknown>;
|
||||
const skillId = String(record.skill_id ?? "").trim();
|
||||
const title = String(record.title ?? "").trim();
|
||||
const rawSkillId = record.skill_id;
|
||||
const skillId =
|
||||
typeof rawSkillId === "string"
|
||||
? rawSkillId.trim()
|
||||
: typeof rawSkillId === "number"
|
||||
? String(rawSkillId)
|
||||
: "";
|
||||
const rawTitle = record.title;
|
||||
const title =
|
||||
typeof rawTitle === "string"
|
||||
? rawTitle.trim()
|
||||
: typeof rawTitle === "number"
|
||||
? String(rawTitle)
|
||||
: "";
|
||||
if (!skillId || !title) return null;
|
||||
return { skill_id: skillId, title };
|
||||
})
|
||||
|
|
@ -84,7 +96,11 @@ export function useIframeSkill(
|
|||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const threadIdFromQuery = searchParams.get("thread_id");
|
||||
const threadId = options?.threadId?.trim() || threadIdFromQuery;
|
||||
const threadIdFromOptions = options?.threadId?.trim();
|
||||
const threadId =
|
||||
threadIdFromOptions && threadIdFromOptions.length > 0
|
||||
? threadIdFromOptions
|
||||
: threadIdFromQuery;
|
||||
const isChattingFromQuery = searchParams.get("is_chatting");
|
||||
const lastThreadIdRef = useRef<string | null>(null);
|
||||
|
||||
|
|
@ -316,7 +332,8 @@ export function useIframeSkill(
|
|||
setSelectedSkills(normalizedSkills);
|
||||
|
||||
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||
description: result.message || t.skills.createdFiles(result.created_files),
|
||||
description:
|
||||
result.message || t.skills.createdFiles(result.created_files),
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
@ -325,7 +342,9 @@ export function useIframeSkill(
|
|||
removeFailedSkills(failedIds);
|
||||
toast.dismiss("suggest-skill-bootstrap");
|
||||
const message =
|
||||
error instanceof Error ? error.message : t.skills.networkRequestFailed;
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t.skills.networkRequestFailed;
|
||||
toast.error(t.skills.loadFailedWithTitle(title), {
|
||||
description: message,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { useSearchParams } from "next/navigation";
|
|||
import { useEffect, useCallback, useState, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
isSelectedSkillMessage,
|
||||
isSelectedSkillsMessage,
|
||||
type SelectedSkillPayloadItem,
|
||||
} from "@/core/iframe-messages";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||
|
||||
/** 技能基础数据 */
|
||||
|
|
@ -105,7 +105,8 @@ export function useSelectedSkillListener({
|
|||
if (result.success) {
|
||||
skillBootstrappedKeyRef.current = initKey;
|
||||
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||
description: result.message || t.skills.createdFiles(result.created_files),
|
||||
description:
|
||||
result.message || t.skills.createdFiles(result.created_files),
|
||||
duration: 4000,
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
|
|
|
|||
|
|
@ -77,27 +77,32 @@
|
|||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
--animate-fade-in: fade-in 1.1s;
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
--animate-fade-in-up: fade-in-up 0.15s ease-in-out forwards;
|
||||
|
||||
@keyframes fade-in-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(1rem) scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
--animate-bouncing: bouncing 0.5s infinite alternate;
|
||||
|
||||
@keyframes bouncing {
|
||||
to {
|
||||
opacity: 0.1;
|
||||
|
|
@ -106,11 +111,13 @@
|
|||
}
|
||||
|
||||
--animate-skeleton-entrance: skeleton-entrance 0.35s ease-out forwards;
|
||||
|
||||
@keyframes skeleton-entrance {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1);
|
||||
|
|
@ -118,11 +125,13 @@
|
|||
}
|
||||
|
||||
--animate-suggestion-in: suggestion-in 0.2s ease-out forwards;
|
||||
|
||||
@keyframes suggestion-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-1.25rem);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -130,17 +139,21 @@
|
|||
}
|
||||
|
||||
--animate-wave: wave 0.6s ease-in-out 2;
|
||||
|
||||
@keyframes wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
|
@ -188,36 +201,45 @@
|
|||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-tooltip-background: var(--tooltip-background);
|
||||
--animate-aurora: aurora 8s ease-in-out infinite alternate;
|
||||
|
||||
@keyframes aurora {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
|
||||
25% {
|
||||
background-position: 50% 100%;
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
transform: rotate(-3deg) scale(0.95);
|
||||
}
|
||||
|
||||
75% {
|
||||
background-position: 50% 0%;
|
||||
transform: rotate(3deg) scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
transform: rotate(-5deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
--animate-shine: shine var(--duration) infinite linear;
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
|
@ -308,22 +330,27 @@
|
|||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
.container-md {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
|
||||
@media (width >=40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
|
||||
@media (width >=48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
|
||||
@media (width >=64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
|
||||
@media (width >=80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -375,9 +402,11 @@
|
|||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 400% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
|
@ -398,13 +427,14 @@
|
|||
|
||||
/* Hide scrollbar but keep scroll behavior */
|
||||
* {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Opera */
|
||||
/* *::-webkit-scrollbar {
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
:root {
|
||||
--container-width-xs: calc(var(--spacing) * 72);
|
||||
|
|
@ -435,6 +465,7 @@ body {
|
|||
p {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 特别指定,代码块和正文一样的字体 */
|
||||
code,
|
||||
kbd,
|
||||
|
|
@ -443,7 +474,8 @@ pre {
|
|||
font-family:
|
||||
"Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
|
||||
}
|
||||
pre{
|
||||
|
||||
pre {
|
||||
border-radius: 5px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
|
@ -462,12 +494,14 @@ pre{
|
|||
|
||||
/* 二三级标题 - 16px */
|
||||
[data-streamdown="heading-2"],
|
||||
[data-streamdown="heading-3"],[data-streamdown="heading-4"] {
|
||||
[data-streamdown="heading-3"],
|
||||
[data-streamdown="heading-4"] {
|
||||
font-size: calc(16px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 代码块 - 14px */
|
||||
[data-streamdown="code-block"] pre,code {
|
||||
[data-streamdown="code-block"] pre,
|
||||
code {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
|
|
@ -482,56 +516,69 @@ pre{
|
|||
[data-streamdown="table-cell"] {
|
||||
background-color: transparent;
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
height:calc(42px * var(--zoom-scale)) ;
|
||||
height: calc(42px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
[data-streamdown="table-header"] {
|
||||
background: #9c9b9b26;
|
||||
height: calc(50px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
[data-streamdown="table-header"] th {
|
||||
text-align: center;
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
[data-slot="hover-card-trigger"] [data-slot="badge"]{
|
||||
|
||||
[data-slot="hover-card-trigger"] [data-slot="badge"] {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
|
||||
/* 表格四角圆角:由四个角单元格承担视觉圆角 */
|
||||
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:first-child {
|
||||
[data-streamdown="table-header"]
|
||||
tr:first-child
|
||||
> [data-streamdown="table-header-cell"]:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:last-child {
|
||||
[data-streamdown="table-header"]
|
||||
tr:first-child
|
||||
> [data-streamdown="table-header-cell"]:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
[data-streamdown="table-body"] tr:first-child td{
|
||||
|
||||
[data-streamdown="table-body"] tr:first-child td {
|
||||
line-height: calc(14px * var(--zoom-scale));
|
||||
padding-top: calc(20px * var(--zoom-scale));
|
||||
}
|
||||
|
||||
/* 行分隔线 */
|
||||
[data-streamdown="table-body"] tr{
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:first-child {
|
||||
/* [data-streamdown="table-body"] tr {
|
||||
border-bottom: 1px solid black;
|
||||
} */
|
||||
|
||||
[data-streamdown="table-body"]
|
||||
tr:last-child
|
||||
> [data-streamdown="table-cell"]:first-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:last-child {
|
||||
[data-streamdown="table-body"]
|
||||
tr:last-child
|
||||
> [data-streamdown="table-cell"]:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
[data-streamdown="table-body"] tr:last-child {
|
||||
padding-top: calc(50px * var(--zoom-scale));
|
||||
|
||||
[data-streamdown="table-body"] tr:last-child td {
|
||||
line-height: calc(14px * var(--zoom-scale));
|
||||
padding-bottom: calc(20px * var(--zoom-scale));
|
||||
}
|
||||
[data-streamdown="table-row"] >[data-streamdown="table-cell"]{
|
||||
line-height: 14px;
|
||||
vertical-align: top;
|
||||
|
||||
[data-streamdown="table-row"] > [data-streamdown="table-cell"] {
|
||||
line-height: calc(42px * var(--zoom-scale));
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.cm-line {
|
||||
font-size: calc(14px * var(--zoom-scale));
|
||||
white-space: pre-wrap;
|
||||
|
|
@ -568,3 +615,7 @@ vertical-align: top;
|
|||
.pptx-preview-wrap .pptx-preview-wrapper {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.ticker-char{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ export async function rewriteFirstReferenceAsArtifact(
|
|||
return false;
|
||||
}
|
||||
|
||||
let fiber = ((element as unknown as Record<string, unknown>)[fiberKey]) as
|
||||
let fiber = (element as unknown as Record<string, unknown>)[fiberKey] as
|
||||
| {
|
||||
return?: unknown;
|
||||
memoizedState?: unknown;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { newChatEntry, openChat, sendMessage } from "./support/chat-helpers";
|
|||
|
||||
function logProgress(message: string) {
|
||||
const timestamp = new Date().toISOString();
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
console.log(`[DF-SEC][${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
|
|
@ -21,10 +21,7 @@ function parseForbiddenPrefixes() {
|
|||
return prefixes;
|
||||
}
|
||||
|
||||
async function assertNoForbiddenPrefixOnScreen(
|
||||
page: Page,
|
||||
prefixes: string[],
|
||||
) {
|
||||
async function assertNoForbiddenPrefixOnScreen(page: Page, prefixes: string[]) {
|
||||
if (prefixes.length === 0) return;
|
||||
const leaked = await page.evaluate((items) => {
|
||||
const text = document.body?.innerText ?? "";
|
||||
|
|
@ -64,9 +61,7 @@ async function waitForConditionWithLeakCheck({
|
|||
const now = Date.now();
|
||||
if (now - lastLogAt >= logEveryMs) {
|
||||
lastLogAt = now;
|
||||
logProgress(
|
||||
`${label}… (${Math.round((now - start) / 1000)}s elapsed)`,
|
||||
);
|
||||
logProgress(`${label}… (${Math.round((now - start) / 1000)}s elapsed)`);
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(stepMs);
|
||||
|
|
@ -113,7 +108,10 @@ async function waitForArtifactCards({
|
|||
label,
|
||||
condition: async () => {
|
||||
// Cards only render when the panel is open. Try to open opportunistically.
|
||||
if ((await fileList.count()) === 0 || !(await fileList.first().isVisible())) {
|
||||
if (
|
||||
(await fileList.count()) === 0 ||
|
||||
!(await fileList.first().isVisible())
|
||||
) {
|
||||
await openArtifactsPanelIfPossible(page);
|
||||
}
|
||||
if ((await cards.count()) < minCount) return false;
|
||||
|
|
@ -169,11 +167,7 @@ async function sendMessageSafely({
|
|||
});
|
||||
await textarea.evaluate((element) => {
|
||||
const target = element as HTMLTextAreaElement;
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
setter?.call(target, "");
|
||||
target.value = "";
|
||||
target.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
||||
});
|
||||
await page.keyboard.insertText(text);
|
||||
|
|
@ -241,7 +235,8 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
|||
timeoutMs: 40_000,
|
||||
label: "Wait for steps signal",
|
||||
condition: async () =>
|
||||
(await stepsSignal.count()) > 0 && (await stepsSignal.first().isVisible()),
|
||||
(await stepsSignal.count()) > 0 &&
|
||||
(await stepsSignal.first().isVisible()),
|
||||
});
|
||||
|
||||
// 按需求:40s 内未出现思考块则中断后续检查(标记为 skip)。
|
||||
|
|
@ -256,9 +251,10 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
|||
minCount: 1,
|
||||
label: "Wait for first artifact card",
|
||||
});
|
||||
expect(firstArtifacts.ok, "未检测到 artifact-file-card,图片可能未生成完成").toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
firstArtifacts.ok,
|
||||
"未检测到 artifact-file-card,图片可能未生成完成",
|
||||
).toBe(true);
|
||||
logProgress(
|
||||
`First artifact ready (count=${await firstArtifacts.cards.count()}).`,
|
||||
);
|
||||
|
|
@ -279,7 +275,10 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
|||
minCount: beforeSecondCount + 1,
|
||||
label: "Wait for second artifact card",
|
||||
});
|
||||
expect(secondArtifacts.ok, "未检测到新的产物生成(artifact 数量未增加)").toBe(true);
|
||||
expect(
|
||||
secondArtifacts.ok,
|
||||
"未检测到新的产物生成(artifact 数量未增加)",
|
||||
).toBe(true);
|
||||
logProgress(
|
||||
`Second artifact ready (count=${await secondArtifacts.cards.count()}).`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Iframe Localhost 2026</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://localhost:2026" title="localhost-2026"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue