Compare commits
No commits in common. "9cecd24918e17c4091210ed6e0348980d69d449b" and "7b53bb0524d5173139dd51aa64905c20be5524e9" have entirely different histories.
9cecd24918
...
7b53bb0524
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"model_profile": "balanced",
|
"model_profile": "balanced",
|
||||||
"commit_docs": false,
|
"commit_docs": true,
|
||||||
"parallelization": true,
|
"parallelization": true,
|
||||||
"search_gitignored": false,
|
"search_gitignored": false,
|
||||||
"brave_search": false,
|
"brave_search": false,
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,12 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
from langchain_core.messages import HumanMessage
|
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 app.gateway.deps import get_checkpointer, get_run_manager, get_store, get_stream_bridge
|
||||||
from deerflow.runtime import (
|
from deerflow.runtime import (
|
||||||
|
|
@ -34,17 +32,6 @@ from deerflow.runtime import (
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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 制作指令,非选择回复。"""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -107,137 +94,6 @@ def normalize_input(raw_input: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
return raw_input
|
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"
|
_DEFAULT_ASSISTANT_ID = "lead_agent"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -426,7 +282,6 @@ async def start_run(
|
||||||
|
|
||||||
agent_factory = resolve_agent_factory(body.assistant_id)
|
agent_factory = resolve_agent_factory(body.assistant_id)
|
||||||
graph_input = normalize_input(body.input)
|
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)
|
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
|
||||||
|
|
||||||
if "configurable" in config and isinstance(config["configurable"], dict):
|
if "configurable" in config and isinstance(config["configurable"], dict):
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
from langchain_core.messages import HumanMessage
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_sse_basic():
|
def test_format_sse_basic():
|
||||||
|
|
@ -84,55 +81,6 @@ def test_normalize_input_passthrough():
|
||||||
assert result == {"custom_key": "value"}
|
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():
|
def test_build_run_config_basic():
|
||||||
from app.gateway.services import build_run_config
|
from app.gateway.services import build_run_config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@
|
||||||
"@uiw/react-codemirror": "^4.25.4",
|
"@uiw/react-codemirror": "^4.25.4",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
"ai": "^6.0.33",
|
"ai": "^6.0.33",
|
||||||
"antd": "^6.3.6",
|
|
||||||
"best-effort-json-parser": "^1.2.1",
|
"best-effort-json-parser": "^1.2.1",
|
||||||
"better-auth": "^1.3",
|
"better-auth": "^1.3",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Ticker } from "@tombcato/smart-ticker";
|
|
||||||
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
@ -23,7 +22,6 @@ import {
|
||||||
} from "@/components/workspace/artifacts";
|
} from "@/components/workspace/artifacts";
|
||||||
import { useThreadChat } from "@/components/workspace/chats";
|
import { useThreadChat } from "@/components/workspace/chats";
|
||||||
// import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
// import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
||||||
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
|
|
@ -41,7 +39,8 @@ import { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener";
|
||||||
import { cn } from "@/lib/utils";
|
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 "@tombcato/smart-ticker/style.css";
|
||||||
import motivationSlogans from "./motivation-slogans.json";
|
import motivationSlogans from "./motivation-slogans.json";
|
||||||
|
|
||||||
|
|
@ -142,7 +141,7 @@ export default function ChatPage() {
|
||||||
if (initializedThreadRef.current === safeThreadId) return;
|
if (initializedThreadRef.current === safeThreadId) return;
|
||||||
initializedThreadRef.current = safeThreadId;
|
initializedThreadRef.current = safeThreadId;
|
||||||
void apiClient.threads
|
void apiClient.threads
|
||||||
// TODO: 先注释先删除再创建的逻辑
|
// TODO: 先注释先删除再创建的逻辑
|
||||||
// .delete(safeThreadId)
|
// .delete(safeThreadId)
|
||||||
// .catch(() => undefined)
|
// .catch(() => undefined)
|
||||||
// .then(() =>
|
// .then(() =>
|
||||||
|
|
@ -490,7 +489,9 @@ export default function ChatPage() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex size-full justify-center px-[20px]">
|
<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 ? (
|
{thread.values.artifacts?.length === 0 ? (
|
||||||
<ConversationEmptyState
|
<ConversationEmptyState
|
||||||
icon={<FilesIcon />}
|
icon={<FilesIcon />}
|
||||||
|
|
@ -499,20 +500,20 @@ export default function ChatPage() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
|
||||||
<header className="flex shrink-0 items-center justify-between border-b">
|
<header className="shrink-0 flex justify-between items-center border-b ">
|
||||||
<h2 className="h-[58px] text-[14px] leading-[58px] font-bold text-[#333333]">
|
<h2 className="text-[14px] h-[58px] leading-[58px] font-bold text-[#333333]">
|
||||||
<span>{t.common.artifacts}</span>
|
<span>{t.common.artifacts}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
data-testid="artifacts-panel-close"
|
data-testid="artifacts-panel-close"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setArtifactsOpen(false);
|
setArtifactsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-0 grow overflow-auto">
|
<main className="min-h-0 grow overflow-auto">
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
|
|
@ -654,9 +655,7 @@ export default function ChatPage() {
|
||||||
<DevDialogContent>
|
<DevDialogContent>
|
||||||
<DevDialogHeader>
|
<DevDialogHeader>
|
||||||
<DevDialogTitle>
|
<DevDialogTitle>
|
||||||
⚠️{" "}
|
⚠️ {selectedSkillError?.title ?? t.chatPage.selectedSkillLoadFailed}
|
||||||
{selectedSkillError?.title ??
|
|
||||||
t.chatPage.selectedSkillLoadFailed}
|
|
||||||
</DevDialogTitle>
|
</DevDialogTitle>
|
||||||
</DevDialogHeader>
|
</DevDialogHeader>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const Message = ({
|
||||||
"group flex w-full flex-col gap-2",
|
"group flex w-full flex-col gap-2",
|
||||||
from === "user"
|
from === "user"
|
||||||
? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6")
|
? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6")
|
||||||
: "is-assistant rounded-[10px] bg-white p-4",
|
: "is-assistant bg-white rounded-[10px] p-4",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -350,19 +350,19 @@ export function PromptInputAttachment({
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/* 删除按钮 - 右上角 */}
|
{/* 删除按钮 - 右上角 */}
|
||||||
<button
|
<button
|
||||||
aria-label={t.common.removeAttachment}
|
aria-label={t.common.removeAttachment}
|
||||||
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-white/20"
|
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-white/20"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onRemove) {
|
if (onRemove) {
|
||||||
onRemove();
|
onRemove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attachments.remove(data.id);
|
attachments.remove(data.id);
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="8"
|
width="8"
|
||||||
|
|
@ -1064,7 +1064,7 @@ export const PromptInputTools = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PromptInputToolsProps) => (
|
}: PromptInputToolsProps) => (
|
||||||
<div className={cn("flex items-center h-full gap-1", className)} {...props} />
|
<div className={cn("flex items-center gap-1", className)} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export const ReasoningContent = memo(
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<div className="break-words whitespace-pre-wrap">{children}</div>
|
<div className="whitespace-pre-wrap break-words">{children}</div>
|
||||||
) : (
|
) : (
|
||||||
<Streamdown
|
<Streamdown
|
||||||
isAnimating={false}
|
isAnimating={false}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react"
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function ContextMenu({
|
function ContextMenu({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuTrigger({
|
function ContextMenuTrigger({
|
||||||
|
|
@ -17,7 +17,7 @@ function ContextMenuTrigger({
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuGroup({
|
function ContextMenuGroup({
|
||||||
|
|
@ -25,7 +25,7 @@ function ContextMenuGroup({
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuPortal({
|
function ContextMenuPortal({
|
||||||
|
|
@ -33,13 +33,13 @@ function ContextMenuPortal({
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSub({
|
function ContextMenuSub({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
}: 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({
|
function ContextMenuRadioGroup({
|
||||||
|
|
@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
|
||||||
data-slot="context-menu-radio-group"
|
data-slot="context-menu-radio-group"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSubTrigger({
|
function ContextMenuSubTrigger({
|
||||||
|
|
@ -59,22 +59,22 @@ function ContextMenuSubTrigger({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.SubTrigger
|
<ContextMenuPrimitive.SubTrigger
|
||||||
data-slot="context-menu-sub-trigger"
|
data-slot="context-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center 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",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto" />
|
<ChevronRightIcon className="ml-auto" />
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSubContent({
|
function ContextMenuSubContent({
|
||||||
|
|
@ -85,12 +85,12 @@ function ContextMenuSubContent({
|
||||||
<ContextMenuPrimitive.SubContent
|
<ContextMenuPrimitive.SubContent
|
||||||
data-slot="context-menu-sub-content"
|
data-slot="context-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuContent({
|
function ContextMenuContent({
|
||||||
|
|
@ -102,13 +102,13 @@ function ContextMenuContent({
|
||||||
<ContextMenuPrimitive.Content
|
<ContextMenuPrimitive.Content
|
||||||
data-slot="context-menu-content"
|
data-slot="context-menu-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</ContextMenuPrimitive.Portal>
|
</ContextMenuPrimitive.Portal>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuItem({
|
function ContextMenuItem({
|
||||||
|
|
@ -117,8 +117,8 @@ function ContextMenuItem({
|
||||||
variant = "default",
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
variant?: "default" | "destructive";
|
variant?: "default" | "destructive"
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Item
|
<ContextMenuPrimitive.Item
|
||||||
|
|
@ -126,12 +126,12 @@ function ContextMenuItem({
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 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",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuCheckboxItem({
|
function ContextMenuCheckboxItem({
|
||||||
|
|
@ -144,8 +144,8 @@ function ContextMenuCheckboxItem({
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
data-slot="context-menu-checkbox-item"
|
data-slot="context-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuRadioItem({
|
function ContextMenuRadioItem({
|
||||||
|
|
@ -169,8 +169,8 @@ function ContextMenuRadioItem({
|
||||||
<ContextMenuPrimitive.RadioItem
|
<ContextMenuPrimitive.RadioItem
|
||||||
data-slot="context-menu-radio-item"
|
data-slot="context-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"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,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -181,7 +181,7 @@ function ContextMenuRadioItem({
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenuPrimitive.RadioItem>
|
</ContextMenuPrimitive.RadioItem>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuLabel({
|
function ContextMenuLabel({
|
||||||
|
|
@ -189,19 +189,19 @@ function ContextMenuLabel({
|
||||||
inset,
|
inset,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
inset?: boolean;
|
inset?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Label
|
<ContextMenuPrimitive.Label
|
||||||
data-slot="context-menu-label"
|
data-slot="context-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
"px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuSeparator({
|
function ContextMenuSeparator({
|
||||||
|
|
@ -211,10 +211,10 @@ function ContextMenuSeparator({
|
||||||
return (
|
return (
|
||||||
<ContextMenuPrimitive.Separator
|
<ContextMenuPrimitive.Separator
|
||||||
data-slot="context-menu-separator"
|
data-slot="context-menu-separator"
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenuShortcut({
|
function ContextMenuShortcut({
|
||||||
|
|
@ -225,12 +225,12 @@ function ContextMenuShortcut({
|
||||||
<span
|
<span
|
||||||
data-slot="context-menu-shortcut"
|
data-slot="context-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -249,4 +249,4 @@ export {
|
||||||
ContextMenuSubContent,
|
ContextMenuSubContent,
|
||||||
ContextMenuSubTrigger,
|
ContextMenuSubTrigger,
|
||||||
ContextMenuRadioGroup,
|
ContextMenuRadioGroup,
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,7 @@ export function DropdownSelector<T extends string>({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="flex w-full items-center justify-center gap-1">
|
<span className="flex w-full items-center justify-center gap-1">
|
||||||
{/* {truncateMiddle(selectedOption?.label ?? value, 20)} */}
|
{truncateMiddle(selectedOption?.label ?? value, 30)}
|
||||||
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
|
|
||||||
|
|
||||||
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
@ -100,8 +98,7 @@ export function DropdownSelector<T extends string>({
|
||||||
value={option.value}
|
value={option.value}
|
||||||
title={option.label}
|
title={option.label}
|
||||||
>
|
>
|
||||||
{/* {truncateMiddle(option.label,50)} */}
|
{truncateMiddle(option.label)}
|
||||||
{truncateMiddle("hfiqwertyuiopasdfghjklxcvbnm.html", 20)}
|
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
const inputGroupAddonVariants = cva(
|
||||||
"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",
|
"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",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
align: {
|
align: {
|
||||||
|
|
@ -46,9 +46,9 @@ const inputGroupAddonVariants = cva(
|
||||||
"inline-end":
|
"inline-end":
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
||||||
"block-start":
|
"block-start":
|
||||||
"order-first w-full justify-start px-3 pt-5 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
||||||
"block-end":
|
"block-end":
|
||||||
"order-last w-full justify-start px-3 py-0 pb-5 group-has-[>input]/input-group:pb-2.5",
|
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,8 @@ import { cn } from "@/lib/utils";
|
||||||
function ScrollArea({
|
function ScrollArea({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
hideScrollbar = true,
|
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
hideScrollbar?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
data-slot="scroll-area"
|
data-slot="scroll-area"
|
||||||
|
|
@ -25,8 +22,8 @@ function ScrollArea({
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar hidden={hideScrollbar} />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner hidden={hideScrollbar} />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react"
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Slider({
|
function Slider({
|
||||||
className,
|
className,
|
||||||
|
|
@ -20,8 +20,8 @@ function Slider({
|
||||||
: Array.isArray(defaultValue)
|
: Array.isArray(defaultValue)
|
||||||
? defaultValue
|
? defaultValue
|
||||||
: [min, max],
|
: [min, max],
|
||||||
[value, defaultValue, min, max],
|
[value, defaultValue, min, max]
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
|
|
@ -32,20 +32,20 @@ function Slider({
|
||||||
max={max}
|
max={max}
|
||||||
className={cn(
|
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",
|
"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}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track
|
<SliderPrimitive.Track
|
||||||
data-slot="slider-track"
|
data-slot="slider-track"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Range
|
<SliderPrimitive.Range
|
||||||
data-slot="slider-range"
|
data-slot="slider-range"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
|
|
@ -53,11 +53,11 @@ function Slider({
|
||||||
<SliderPrimitive.Thumb
|
<SliderPrimitive.Thumb
|
||||||
data-slot="slider-thumb"
|
data-slot="slider-thumb"
|
||||||
key={index}
|
key={index}
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Slider };
|
export { Slider }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import ExcelJS from "exceljs";
|
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
|
@ -18,6 +17,7 @@ import {
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Streamdown } from "streamdown";
|
import { Streamdown } from "streamdown";
|
||||||
|
import ExcelJS from "exceljs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Artifact,
|
Artifact,
|
||||||
|
|
@ -73,11 +73,13 @@ let revoGridLoaderPromise: Promise<void> | null = null;
|
||||||
function ensureRevoGridDefined() {
|
function ensureRevoGridDefined() {
|
||||||
if (typeof window === "undefined") return Promise.resolve();
|
if (typeof window === "undefined") return Promise.resolve();
|
||||||
if (window.customElements.get("revo-grid")) return Promise.resolve();
|
if (window.customElements.get("revo-grid")) return Promise.resolve();
|
||||||
revoGridLoaderPromise ??= import("@revolist/revogrid/loader").then(
|
if (!revoGridLoaderPromise) {
|
||||||
({ defineCustomElements }) => {
|
revoGridLoaderPromise = import("@revolist/revogrid/loader").then(
|
||||||
defineCustomElements(window);
|
({ defineCustomElements }) => {
|
||||||
},
|
defineCustomElements(window);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
return revoGridLoaderPromise;
|
return revoGridLoaderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,45 +99,18 @@ function toGridCellText(cell: ExcelJS.Cell): string {
|
||||||
const value = cell.value;
|
const value = cell.value;
|
||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
if (value instanceof Date) return value.toISOString();
|
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 (typeof value === "object") {
|
||||||
if ("result" in value && value.result != null) {
|
if ("result" in value && value.result != null) {
|
||||||
const result = value.result;
|
return String(value.result);
|
||||||
if (
|
|
||||||
typeof result === "string" ||
|
|
||||||
typeof result === "number" ||
|
|
||||||
typeof result === "boolean" ||
|
|
||||||
typeof result === "bigint"
|
|
||||||
) {
|
|
||||||
return String(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ("text" in value && value.text) {
|
if ("text" in value && value.text) {
|
||||||
const text = value.text;
|
return String(value.text);
|
||||||
if (
|
|
||||||
typeof text === "string" ||
|
|
||||||
typeof text === "number" ||
|
|
||||||
typeof text === "boolean" ||
|
|
||||||
typeof text === "bigint"
|
|
||||||
) {
|
|
||||||
return String(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ("hyperlink" in value && value.hyperlink) {
|
if ("hyperlink" in value && value.hyperlink) {
|
||||||
const hyperlink = value.hyperlink;
|
return String(value.hyperlink);
|
||||||
if (typeof hyperlink === "string") {
|
|
||||||
return hyperlink;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
|
function toRevoGridSheetData(worksheet: ExcelJS.Worksheet): RevoGridSheetData {
|
||||||
|
|
@ -423,8 +398,8 @@ export function ArtifactFileDetail({
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ArtifactHeader className="grid grid-cols-24">
|
<ArtifactHeader className="grid grid-cols-12 gap-3">
|
||||||
<div className="col-span-7 flex min-w-0 items-center justify-start gap-2 overflow-hidden">
|
<div className="col-span-3 flex min-w-0 items-center justify-start gap-2 overflow-hidden">
|
||||||
{previewable && (
|
{previewable && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
|
@ -489,11 +464,11 @@ export function ArtifactFileDetail({
|
||||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-10 flex min-w-0 items-center justify-center px-1">
|
<div className="col-span-6 flex min-w-0 items-center justify-center px-1">
|
||||||
<ArtifactTitle>
|
<ArtifactTitle>
|
||||||
{isWriteFile ? (
|
{isWriteFile ? (
|
||||||
<div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
|
<div className="w-full overflow-hidden px-2 text-center text-ellipsis whitespace-nowrap">
|
||||||
{truncateMiddle(getFileName(filepath), 20)}
|
{truncateMiddle(getFileName(filepath), 50)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DropdownSelector
|
<DropdownSelector
|
||||||
|
|
@ -504,7 +479,7 @@ export function ArtifactFileDetail({
|
||||||
)}
|
)}
|
||||||
</ArtifactTitle>
|
</ArtifactTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-7 flex min-w-0 items-center justify-end overflow-hidden">
|
<div className="col-span-3 flex min-w-0 items-center justify-end overflow-hidden">
|
||||||
<ArtifactActions>
|
<ArtifactActions>
|
||||||
{isCodeFile && (
|
{isCodeFile && (
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
|
|
@ -1727,18 +1702,12 @@ export const ArtifactZoomSelector = ({
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent align="start" sideOffset={8} className="w-52 p-[20px] ">
|
||||||
align="start"
|
|
||||||
sideOffset={8}
|
|
||||||
className="w-52 p-[20px]"
|
|
||||||
>
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
{ZOOM_LEVELS[0]}%
|
{ZOOM_LEVELS[0]}%
|
||||||
</span>
|
</span>
|
||||||
<span className="text-foreground text-xs font-medium">
|
<span className="text-foreground text-xs font-medium">{value}%</span>
|
||||||
{value}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,7 @@ const ChatBox: React.FC<{
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
||||||
<header className="shrink-0">
|
<header className="shrink-0">
|
||||||
<h2 className="text-lg font-medium">
|
<h2 className="text-lg font-medium">{t.common.artifacts}</h2>
|
||||||
{t.common.artifacts}
|
|
||||||
</h2>
|
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-0 grow">
|
<main className="min-h-0 grow">
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import type { ChatStatus } from "ai";
|
import type { ChatStatus } from "ai";
|
||||||
import { Tour } from "antd";
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
|
|
@ -14,11 +15,8 @@ import {
|
||||||
XIcon,
|
XIcon,
|
||||||
ZapIcon,
|
ZapIcon,
|
||||||
} from "lucide-react";
|
} 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 { useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
|
@ -27,9 +25,7 @@ import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type ComponentProps,
|
type ComponentProps,
|
||||||
type RefObject,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PromptInput,
|
PromptInput,
|
||||||
|
|
@ -75,14 +71,15 @@ import { useI18n } from "@/core/i18n/hooks";
|
||||||
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
|
import type { SelectedSkillPayloadItem } from "@/core/i18n/locales/types";
|
||||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import type { AgentThreadContext } from "@/core/threads";
|
|
||||||
import {
|
import {
|
||||||
MENTION_REFERENCE_EVENT,
|
MENTION_REFERENCE_EVENT,
|
||||||
type MentionReferenceEventDetail,
|
type MentionReferenceEventDetail,
|
||||||
} from "@/core/threads/reference-events";
|
} from "@/core/threads/reference-events";
|
||||||
|
import type { AgentThreadContext } from "@/core/threads";
|
||||||
import { useUploadedFiles } from "@/core/uploads/hooks";
|
import { useUploadedFiles } from "@/core/uploads/hooks";
|
||||||
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
import { useIframeSkill } from "@/hooks/use-iframe-skill";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ModelSelector,
|
ModelSelector,
|
||||||
|
|
@ -94,33 +91,13 @@ import {
|
||||||
ModelSelectorTrigger,
|
ModelSelectorTrigger,
|
||||||
} from "../ai-elements/model-selector";
|
} from "../ai-elements/model-selector";
|
||||||
import { Suggestion, Suggestions } from "../ai-elements/suggestion";
|
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 { ModeHoverGuide } from "./mode-hover-guide";
|
||||||
import { Tooltip } from "./tooltip";
|
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 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 = {
|
type MentionCandidate = {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -237,10 +214,6 @@ export function InputBox({
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mentionTriggerRef = useRef<HTMLButtonElement | 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 [followups, setFollowups] = useState<string[]>([]);
|
||||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||||
|
|
@ -257,97 +230,11 @@ export function InputBox({
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isInputToolsTourOpen, setIsInputToolsTourOpen] = useState(false);
|
|
||||||
const [isInputToolsTourReady, setIsInputToolsTourReady] = useState(false);
|
|
||||||
const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps);
|
const { data: uploadedFilesData } = useUploadedFiles(threadIdFromProps);
|
||||||
|
|
||||||
// isNewThread 时禁用收缩,始终保持展开(除非已提交消息)
|
// isNewThread 时禁用收缩,始终保持展开(除非已提交消息)
|
||||||
const effectiveIsFocused =
|
const effectiveIsFocused =
|
||||||
((showWelcomeStyle ?? false) && !hasSubmitted) || isFocused;
|
((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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -399,9 +286,9 @@ export function InputBox({
|
||||||
isImage: isImageFilename(filename),
|
isImage: isImageFilename(filename),
|
||||||
previewUrl: threadId
|
previewUrl: threadId
|
||||||
? urlOfArtifact({
|
? urlOfArtifact({
|
||||||
filepath: path,
|
filepath: path,
|
||||||
threadId,
|
threadId,
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -485,14 +372,7 @@ export function InputBox({
|
||||||
});
|
});
|
||||||
setReferences([]);
|
setReferences([]);
|
||||||
},
|
},
|
||||||
[
|
[showWelcomeStyle, onSubmit, onStop, references, status, iframeSkill.selectedSkills],
|
||||||
showWelcomeStyle,
|
|
||||||
onSubmit,
|
|
||||||
onStop,
|
|
||||||
references,
|
|
||||||
status,
|
|
||||||
iframeSkill.selectedSkills,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestFormSubmit = useCallback(() => {
|
const requestFormSubmit = useCallback(() => {
|
||||||
|
|
@ -500,27 +380,24 @@ export function InputBox({
|
||||||
form?.requestSubmit();
|
form?.requestSubmit();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addMentionReference = useCallback(
|
const addMentionReference = useCallback((reference: PromptInputReference) => {
|
||||||
(reference: PromptInputReference) => {
|
setReferences((prev) => {
|
||||||
setReferences((prev) => {
|
const exists = prev.some(
|
||||||
const exists = prev.some(
|
(item) =>
|
||||||
(item) =>
|
item.ref_source === reference.ref_source &&
|
||||||
item.ref_source === reference.ref_source &&
|
item.path === reference.path &&
|
||||||
item.path === reference.path &&
|
item.filename === reference.filename,
|
||||||
item.filename === reference.filename,
|
);
|
||||||
);
|
if (exists) {
|
||||||
if (exists) {
|
return prev;
|
||||||
return prev;
|
}
|
||||||
}
|
if (prev.length >= MAX_REFERENCES_PER_MESSAGE) {
|
||||||
if (prev.length >= MAX_REFERENCES_PER_MESSAGE) {
|
toast.error(t.inputBox.maxReferencesReached);
|
||||||
toast.error(t.inputBox.maxReferencesReached);
|
return prev;
|
||||||
return prev;
|
}
|
||||||
}
|
return prev.concat(reference);
|
||||||
return prev.concat(reference);
|
});
|
||||||
});
|
}, [t.inputBox.maxReferencesReached]);
|
||||||
},
|
|
||||||
[t.inputBox.maxReferencesReached],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectMentionCandidate = useCallback(
|
const selectMentionCandidate = useCallback(
|
||||||
(candidate: MentionCandidate) => {
|
(candidate: MentionCandidate) => {
|
||||||
|
|
@ -555,7 +432,7 @@ export function InputBox({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onMentionReference = (event: Event) => {
|
const onMentionReference = (event: Event) => {
|
||||||
const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail;
|
const detail = (event as CustomEvent<MentionReferenceEventDetail>).detail;
|
||||||
if (detail?.threadId !== threadIdFromProps) {
|
if (!detail || detail.threadId !== threadIdFromProps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addMentionReference({
|
addMentionReference({
|
||||||
|
|
@ -615,15 +492,14 @@ export function InputBox({
|
||||||
}
|
}
|
||||||
if (event.key === "ArrowDown") {
|
if (event.key === "ArrowDown") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setActiveMentionIndex(
|
setActiveMentionIndex((prev) =>
|
||||||
(prev) => (prev + 1) % filteredMentionCandidates.length,
|
(prev + 1) % filteredMentionCandidates.length,
|
||||||
);
|
);
|
||||||
} else if (event.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setActiveMentionIndex(
|
setActiveMentionIndex((prev) =>
|
||||||
(prev) =>
|
(prev - 1 + filteredMentionCandidates.length) %
|
||||||
(prev - 1 + filteredMentionCandidates.length) %
|
filteredMentionCandidates.length,
|
||||||
filteredMentionCandidates.length,
|
|
||||||
);
|
);
|
||||||
} else if (event.key === "Enter") {
|
} else if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -727,21 +603,6 @@ export function InputBox({
|
||||||
}}
|
}}
|
||||||
className="relative w-full"
|
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
|
<AttachmentPreviewBar
|
||||||
references={references}
|
references={references}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
|
|
@ -797,11 +658,7 @@ export function InputBox({
|
||||||
!effectiveIsFocused && "h-[80px] py-0 leading-20",
|
!effectiveIsFocused && "h-[80px] py-0 leading-20",
|
||||||
)}
|
)}
|
||||||
disabled={isInputDisabled}
|
disabled={isInputDisabled}
|
||||||
placeholder={
|
placeholder={t.inputBox.placeholder}
|
||||||
showWelcomeStyle
|
|
||||||
? t.inputBox.welcomePlaceholder
|
|
||||||
: t.inputBox.chatPlaceholder
|
|
||||||
}
|
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
defaultValue={initialValue}
|
defaultValue={initialValue}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
|
|
@ -831,7 +688,7 @@ export function InputBox({
|
||||||
align="start"
|
align="start"
|
||||||
side="top"
|
side="top"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
className="max-h-[400px] w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] overflow-y-hidden p-[20px]"
|
className="w-[min(32rem,var(--radix-dropdown-menu-trigger-width)+28rem)] max-h-[400px] overflow-y-visible p-[20px]"
|
||||||
data-testid="mention-candidate-panel"
|
data-testid="mention-candidate-panel"
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -842,55 +699,51 @@ export function InputBox({
|
||||||
{t.inputBox.addReference}
|
{t.inputBox.addReference}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
||||||
<DropdownMenuGroup className="flex max-h-[480px] flex-col gap-[10px] px-0 pt-[20px]">
|
<DropdownMenuGroup className="flex pt-[20px] px-0 max-h-[480px] flex-col gap-[10px] overflow-y-auto">
|
||||||
<ScrollArea className="h-[480px]" data-state="hidden">
|
{filteredMentionCandidates.slice(0, 20).map((candidate, index) => {
|
||||||
{filteredMentionCandidates.map((candidate, index) => {
|
const detail = [candidate.typeLabel, candidate.pathTail]
|
||||||
const detail = [candidate.typeLabel, candidate.pathTail]
|
.filter(Boolean)
|
||||||
.filter(Boolean)
|
.join(" · ");
|
||||||
.join(" · ");
|
return (
|
||||||
return (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem
|
key={candidate.key}
|
||||||
key={candidate.key}
|
className={cn(
|
||||||
className={cn(
|
"flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left",
|
||||||
"flex items-center justify-between gap-3 rounded-md px-2 py-2 text-left",
|
index === activeMentionIndex && "bg-accent",
|
||||||
index === activeMentionIndex && "bg-accent",
|
)}
|
||||||
)}
|
data-active={index === activeMentionIndex ? "true" : "false"}
|
||||||
data-active={
|
data-candidate-key={candidate.key}
|
||||||
index === activeMentionIndex ? "true" : "false"
|
data-testid="mention-candidate-item"
|
||||||
}
|
aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`}
|
||||||
data-candidate-key={candidate.key}
|
onFocus={() => setActiveMentionIndex(index)}
|
||||||
data-testid="mention-candidate-item"
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
aria-label={`${candidate.filename} ${candidate.typeLabel}${candidate.pathTail ? ` ${candidate.pathTail}` : ""}`}
|
onSelect={(event) => {
|
||||||
onFocus={() => setActiveMentionIndex(index)}
|
event.preventDefault();
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
selectMentionCandidate(candidate);
|
||||||
onSelect={(event) => {
|
}}
|
||||||
event.preventDefault();
|
>
|
||||||
selectMentionCandidate(candidate);
|
{candidate.isImage && candidate.previewUrl ? (
|
||||||
}}
|
<img
|
||||||
>
|
src={candidate.previewUrl}
|
||||||
{candidate.isImage && candidate.previewUrl ? (
|
alt={candidate.filename}
|
||||||
<img
|
className="h-10 w-10 shrink-0 rounded-md border object-cover object-top"
|
||||||
src={candidate.previewUrl}
|
/>
|
||||||
alt={candidate.filename}
|
) : (
|
||||||
className="h-10 w-10 shrink-0 rounded-md border object-cover object-top"
|
<div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-md border text-[10px] font-semibold">
|
||||||
/>
|
{fileExtensionLabel(candidate.filename)}
|
||||||
) : (
|
</div>
|
||||||
<div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-md border text-[10px] font-semibold">
|
)}
|
||||||
{fileExtensionLabel(candidate.filename)}
|
<div className="min-w-0 flex-1">
|
||||||
</div>
|
<span className="block truncate text-sm font-medium">
|
||||||
)}
|
{candidate.filename}
|
||||||
<div className="min-w-0 flex-1">
|
</span>
|
||||||
<span className="block truncate text-sm font-medium">
|
<span className="text-muted-foreground block truncate text-xs">
|
||||||
{candidate.filename}
|
{detail}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground block truncate text-xs">
|
</div>
|
||||||
{detail}
|
</DropdownMenuItem>
|
||||||
</span>
|
);
|
||||||
</div>
|
})}
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollArea>
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
@ -911,7 +764,7 @@ export function InputBox({
|
||||||
"pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0",
|
"pointer-events-none invisible h-[0px] translate-y-2 p-[0px] opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PromptInputTools className="min-w-0 w-full overflow-hidden gap-[20px]">
|
<PromptInputTools className="min-w-0 flex-1 gap-[20px]">
|
||||||
{/* TODO: Add more connectors here
|
{/* TODO: Add more connectors here
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger className="px-2!" />
|
<PromptInputActionMenuTrigger className="px-2!" />
|
||||||
|
|
@ -921,27 +774,19 @@ export function InputBox({
|
||||||
/>
|
/>
|
||||||
</PromptInputActionMenuContent>
|
</PromptInputActionMenuContent>
|
||||||
</PromptInputActionMenu> */}
|
</PromptInputActionMenu> */}
|
||||||
{showWelcomeStyle && (
|
{showWelcomeStyle && <HistoryButton
|
||||||
<div ref={historyButtonTourRef} className="shrink-0 h-full">
|
className="px-2!"
|
||||||
<HistoryButton
|
router={router}
|
||||||
router={router}
|
threadId={threadIdFromProps}
|
||||||
threadId={threadIdFromProps}
|
/>}
|
||||||
/>
|
<AddAttachmentsButton className="px-2!" />
|
||||||
</div>
|
<IframeSkillDialogButton
|
||||||
)}
|
className="px-2!"
|
||||||
<div ref={attachmentsButtonTourRef} className="shrink-0 h-full">
|
selectedSkills={iframeSkill.selectedSkills}
|
||||||
<AddAttachmentsButton />
|
isBootstrapping={iframeSkill.isBootstrapping}
|
||||||
</div>
|
openSkillDialog={iframeSkill.openSkillDialog}
|
||||||
<div className="min-w-0 grow basis-0 h-full">
|
clearSkill={iframeSkill.clearSkill}
|
||||||
<IframeSkillDialogButton
|
/>
|
||||||
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 版本隐藏运行模式切换按钮 */}
|
{/* 参考 kexue 版本隐藏运行模式切换按钮 */}
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
|
|
@ -978,7 +823,7 @@ export function InputBox({
|
||||||
</ModelSelector> */}
|
</ModelSelector> */}
|
||||||
<PromptInputTools>
|
<PromptInputTools>
|
||||||
{/* 占位符 */}
|
{/* 占位符 */}
|
||||||
<div className="w-[150px] h-[40px]"></div>
|
<div className="w-[150px]"></div>
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
</PromptInputFooter>
|
</PromptInputFooter>
|
||||||
<PromptInputSubmit
|
<PromptInputSubmit
|
||||||
|
|
@ -989,9 +834,10 @@ export function InputBox({
|
||||||
/>
|
/>
|
||||||
</PromptInput>
|
</PromptInput>
|
||||||
|
|
||||||
{shouldShowSuggestionList && (
|
{showWelcomeStyle &&
|
||||||
|
!hasSubmitted &&
|
||||||
|
searchParams.get("mode") !== "skill" && (
|
||||||
<SuggestionListContainer
|
<SuggestionListContainer
|
||||||
ref={suggestionListTourRef}
|
|
||||||
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
|
bootstrapAndLockSkills={iframeSkill.bootstrapAndLockSkills}
|
||||||
isBootstrapping={iframeSkill.isBootstrapping}
|
isBootstrapping={iframeSkill.isBootstrapping}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1058,29 +904,25 @@ export function InputBox({
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuggestionList 容器
|
// SuggestionList 容器
|
||||||
const SuggestionListContainer = forwardRef<HTMLDivElement, {
|
function SuggestionListContainer({
|
||||||
|
bootstrapAndLockSkills,
|
||||||
|
isBootstrapping,
|
||||||
|
}: {
|
||||||
bootstrapAndLockSkills: (params: {
|
bootstrapAndLockSkills: (params: {
|
||||||
selectedSkills: SelectedSkillPayloadItem[];
|
selectedSkills: SelectedSkillPayloadItem[];
|
||||||
title: string;
|
title: string;
|
||||||
}) => Promise<boolean>;
|
}) => Promise<boolean>;
|
||||||
isBootstrapping: boolean;
|
isBootstrapping: boolean;
|
||||||
}>(
|
}) {
|
||||||
function SuggestionListContainer(
|
return (
|
||||||
{ 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 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
|
<SuggestionList
|
||||||
bootstrapAndLockSkills={bootstrapAndLockSkills}
|
bootstrapAndLockSkills={bootstrapAndLockSkills}
|
||||||
isBootstrapping={isBootstrapping}
|
isBootstrapping={isBootstrapping}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// 快速选择skillbutton
|
// 快速选择skillbutton
|
||||||
function SuggestionList({
|
function SuggestionList({
|
||||||
|
|
@ -1182,29 +1024,29 @@ function AddAttachmentsButton({ className }: { className?: string }) {
|
||||||
const attachments = usePromptInputAttachments();
|
const attachments = usePromptInputAttachments();
|
||||||
return (
|
return (
|
||||||
<Tooltip content={t.inputBox.addAttachments}>
|
<Tooltip content={t.inputBox.addAttachments}>
|
||||||
<WorkspaceToolButton
|
<PromptInputButton
|
||||||
className={className}
|
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||||
onClick={() => attachments.openFileDialog()}
|
onClick={() => attachments.openFileDialog()}
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="15"
|
|
||||||
viewBox="0 0 18 15"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z"
|
width="18"
|
||||||
fill="#150033"
|
height="15"
|
||||||
/>
|
viewBox="0 0 18 15"
|
||||||
<path
|
fill="none"
|
||||||
d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke="#150033"
|
className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]"
|
||||||
/>
|
>
|
||||||
</svg>
|
<path
|
||||||
</WorkspaceToolButton>
|
d="M7.05042 7.65254C6.9754 7.72756 6.90039 7.80257 6.90039 7.95258C6.90039 8.02759 6.9754 8.1776 7.05042 8.25262C7.20043 8.40263 7.42545 8.40263 7.57546 8.25262L8.8506 6.97747V10.7279C8.8506 10.9529 9.00061 11.1029 9.22563 11.1029C9.30065 11.1029 9.45066 11.0279 9.52567 11.0279C9.60067 10.9529 9.67568 10.8779 9.67568 10.7279V6.97747L10.9508 8.25262C11.1008 8.40263 11.3259 8.40263 11.4759 8.25262C11.5509 8.1776 11.6259 8.10259 11.6259 7.95258C11.6259 7.87757 11.5509 7.72756 11.4759 7.65254L9.52567 5.70235C9.37564 5.55234 9.15062 5.55234 9.00061 5.70235L7.05042 7.65254Z"
|
||||||
</Tooltip>
|
fill="#150033"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1.12695 0.5H6.67871C6.87077 0.500077 7.01409 0.574515 7.07324 0.648438L7.09082 0.669922L8.30762 1.88672C8.6222 2.20119 9.01344 2.3681 9.44629 2.36816H16.875C17.2382 2.36842 17.5012 2.63339 17.5 2.99414V13.8848C17.5048 14.2408 17.2454 14.5056 16.8818 14.5059H1.12695C0.764649 14.5057 0.5 14.2401 0.5 13.877V1.12793C0.500049 0.810129 0.702664 0.567404 0.996094 0.511719L1.12695 0.5Z"
|
||||||
|
stroke="#150033"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</PromptInputButton>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1220,48 +1062,47 @@ function HistoryButton({
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Tooltip content={t.inputBox.history}>
|
<Tooltip content={t.inputBox.history}>
|
||||||
<WorkspaceToolButton
|
<PromptInputButton
|
||||||
className={className}
|
className={cn("group px-2! hover:bg-[#EAE2F5]", className)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.replace(`/workspace/chats/${threadId}?is_chatting=true`)
|
router.replace(`/workspace/chats/${threadId}?is_chatting=true`)
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="transition-[stroke] duration-200"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<circle
|
<svg
|
||||||
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
|
className="transition-[stroke] duration-200"
|
||||||
cx="9"
|
width="18"
|
||||||
cy="9"
|
height="18"
|
||||||
r="8.5"
|
viewBox="0 0 18 18"
|
||||||
/>
|
fill="none"
|
||||||
<path
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
|
>
|
||||||
d="M9 6V10H12"
|
<circle
|
||||||
strokeLinecap="round"
|
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
|
||||||
strokeLinejoin="round"
|
cx="9"
|
||||||
/>
|
cy="9"
|
||||||
</svg>
|
r="8.5"
|
||||||
</WorkspaceToolButton>
|
/>
|
||||||
|
<path
|
||||||
|
className="stroke-[#150033] transition-[stroke] duration-200 group-hover:stroke-[#8E47F0]"
|
||||||
|
d="M9 6V10H12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 启动iframeSkillDialog
|
// 启动iframeSkillDialog
|
||||||
function IframeSkillDialogButton({
|
function IframeSkillDialogButton({
|
||||||
className,
|
className,
|
||||||
skillButtonRef,
|
|
||||||
selectedSkills,
|
selectedSkills,
|
||||||
isBootstrapping,
|
isBootstrapping,
|
||||||
openSkillDialog,
|
openSkillDialog,
|
||||||
clearSkill,
|
clearSkill,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
skillButtonRef?: RefObject<HTMLDivElement | null>;
|
|
||||||
selectedSkills: Array<{ skill_id: string; title: string }>;
|
selectedSkills: Array<{ skill_id: string; title: string }>;
|
||||||
isBootstrapping: boolean;
|
isBootstrapping: boolean;
|
||||||
openSkillDialog: () => void;
|
openSkillDialog: () => void;
|
||||||
|
|
@ -1270,26 +1111,24 @@ function IframeSkillDialogButton({
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 w-full items-center h-full gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
<Tooltip content={t.inputBox.selectSkill}>
|
<Tooltip content={t.inputBox.selectSkill}>
|
||||||
<div ref={skillButtonRef} className="shrink-0">
|
<PromptInputButton
|
||||||
<WorkspaceToolButton
|
className={cn("group shrink-0 px-2! hover:bg-[#EAE2F5]", className)}
|
||||||
className={cn("shrink-0", className)}
|
onClick={openSkillDialog}
|
||||||
onClick={openSkillDialog}
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]"
|
||||||
|
viewBox="0 0 12 16"
|
||||||
|
fill="none"
|
||||||
>
|
>
|
||||||
<svg
|
<path
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z"
|
||||||
className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]"
|
stroke="#150033"
|
||||||
viewBox="0 0 12 16"
|
/>
|
||||||
fill="none"
|
</svg>
|
||||||
>
|
</PromptInputButton>
|
||||||
<path
|
|
||||||
d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z"
|
|
||||||
stroke="#150033"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</WorkspaceToolButton>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isBootstrapping ? (
|
{isBootstrapping ? (
|
||||||
<Tag className="bg-background text-muted-foreground gap-2 border">
|
<Tag className="bg-background text-muted-foreground gap-2 border">
|
||||||
|
|
@ -1299,7 +1138,7 @@ function IframeSkillDialogButton({
|
||||||
) : null}
|
) : null}
|
||||||
{!isBootstrapping && selectedSkills.length > 0 ? (
|
{!isBootstrapping && selectedSkills.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
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"
|
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"
|
||||||
onWheel={(event) => {
|
onWheel={(event) => {
|
||||||
if (event.deltaY === 0) return;
|
if (event.deltaY === 0) return;
|
||||||
event.currentTarget.scrollLeft += event.deltaY;
|
event.currentTarget.scrollLeft += event.deltaY;
|
||||||
|
|
@ -1359,20 +1198,17 @@ function AttachmentPreviewBar({
|
||||||
</PromptInputAttachments>
|
</PromptInputAttachments>
|
||||||
)}
|
)}
|
||||||
{hasReferences && (
|
{hasReferences && (
|
||||||
<div
|
<div className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2" data-testid="reference-inline-preview">
|
||||||
className="inline-flex flex-row flex-wrap items-center gap-2 rounded-xl p-2"
|
|
||||||
data-testid="reference-inline-preview"
|
|
||||||
>
|
|
||||||
{references.map((reference) => {
|
{references.map((reference) => {
|
||||||
const referenceUrl =
|
const referenceUrl =
|
||||||
threadId && reference.path
|
threadId && reference.path
|
||||||
? urlOfArtifact({
|
? urlOfArtifact({
|
||||||
filepath: reference.path,
|
filepath: reference.path,
|
||||||
threadId,
|
threadId,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const filename = reference.filename ?? "reference";
|
const filename = reference.filename ?? "reference";
|
||||||
const imageMatch = /\.(png|jpe?g|gif|webp|bmp|svg)$/i.exec(filename);
|
const imageMatch = filename.match(/\.(png|jpe?g|gif|webp|bmp|svg)$/i);
|
||||||
const extension = imageMatch?.[1]?.toLowerCase();
|
const extension = imageMatch?.[1]?.toLowerCase();
|
||||||
const mediaType = extension
|
const mediaType = extension
|
||||||
? extension === "jpg"
|
? extension === "jpg"
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
import { useMemo } from "react";
|
||||||
import { useCallback, useMemo, useState, type MouseEvent } from "react";
|
import type { AnchorHTMLAttributes } from "react";
|
||||||
import type {
|
|
||||||
AnchorHTMLAttributes,
|
|
||||||
ComponentPropsWithoutRef,
|
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MessageResponse,
|
MessageResponse,
|
||||||
type MessageResponseProps,
|
type MessageResponseProps,
|
||||||
} from "@/components/ai-elements/message";
|
} from "@/components/ai-elements/message";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
import { cn, copyToClipboard } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { CitationLink } from "../citations/citation-link";
|
import { CitationLink } from "../citations/citation-link";
|
||||||
|
|
||||||
|
|
@ -31,97 +25,6 @@ export type MarkdownContentProps = {
|
||||||
components?: MessageResponseProps["components"];
|
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. */
|
/** Renders markdown content. */
|
||||||
export function MarkdownContent({
|
export function MarkdownContent({
|
||||||
content,
|
content,
|
||||||
|
|
@ -131,8 +34,6 @@ export function MarkdownContent({
|
||||||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||||
components: componentsFromProps,
|
components: componentsFromProps,
|
||||||
}: MarkdownContentProps) {
|
}: MarkdownContentProps) {
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
|
|
@ -157,23 +58,9 @@ 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]);
|
}, [componentsFromProps]);
|
||||||
|
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
|
|
@ -181,7 +68,6 @@ export function MarkdownContent({
|
||||||
<MessageResponse
|
<MessageResponse
|
||||||
className={className}
|
className={className}
|
||||||
isAnimating={isLoading}
|
isAnimating={isLoading}
|
||||||
controls={{ table: false }}
|
|
||||||
parseIncompleteMarkdown={!isLoading}
|
parseIncompleteMarkdown={!isLoading}
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
import { MarkdownContent } from "./markdown-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
|
|
||||||
|
|
||||||
export function MessageGroup({
|
export function MessageGroup({
|
||||||
className,
|
className,
|
||||||
messages,
|
messages,
|
||||||
|
|
@ -86,7 +87,11 @@ export function MessageGroup({
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(false);
|
||||||
const thinkingComponents = useMemo(
|
const thinkingComponents = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
code: ({ className, children, ...props }: ComponentProps<"code">) => {
|
code: ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<"code">) => {
|
||||||
const isBlock =
|
const isBlock =
|
||||||
typeof className === "string" && className.includes("language-");
|
typeof className === "string" && className.includes("language-");
|
||||||
if (!isBlock) {
|
if (!isBlock) {
|
||||||
|
|
@ -121,7 +126,7 @@ export function MessageGroup({
|
||||||
<Button
|
<Button
|
||||||
key="above"
|
key="above"
|
||||||
// 等宋
|
// 等宋
|
||||||
className="h-auto! w-full items-start justify-start py-4 text-left"
|
className="w-full items-start justify-start text-left h-auto! py-4"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@ import {
|
||||||
stripUploadedFilesTag,
|
stripUploadedFilesTag,
|
||||||
type FileInMessage,
|
type FileInMessage,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
|
import { dispatchMentionReference } from "@/core/threads/reference-events";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
import { materializeSkillYaml } from "@/core/skills";
|
import { materializeSkillYaml } from "@/core/skills";
|
||||||
import { humanMessagePlugins } from "@/core/streamdown";
|
import { humanMessagePlugins } from "@/core/streamdown";
|
||||||
import { dispatchMentionReference } from "@/core/threads/reference-events";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { CopyButton } from "../copy-button";
|
import { CopyButton } from "../copy-button";
|
||||||
|
|
@ -424,22 +424,22 @@ function RichFileCard({
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent className="min-w-[120px]">
|
<ContextMenuContent className="min-w-[120px]">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
disabled={!canReference}
|
disabled={!canReference}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!file.path) return;
|
if (!file.path) return;
|
||||||
dispatchMentionReference({
|
dispatchMentionReference({
|
||||||
threadId,
|
threadId,
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
path: file.path,
|
path: file.path,
|
||||||
ref_source: refSource,
|
ref_source: refSource,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t.common.reference}
|
{t.common.reference}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
||||||
) : (
|
) : (
|
||||||
<div className="text-primary ml-2 cursor-default font-serif">
|
<div className="text-primary ml-2 cursor-default font-serif">
|
||||||
{/* TODO: 测试标识 */}
|
{/* TODO: 测试标识 */}
|
||||||
XClaw <span className="text-sm text-[#000000c5]">v3.2.8</span>
|
XClaw <span className="text-sm text-[#000000c5]">v3.2.7</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,6 @@ export const enUS: Translations = {
|
||||||
// Input Box
|
// Input Box
|
||||||
inputBox: {
|
inputBox: {
|
||||||
placeholder: "How can I assist you today?",
|
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:
|
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?",
|
"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:
|
sendMessagePrice:
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,6 @@ export interface Translations {
|
||||||
inputBox: {
|
inputBox: {
|
||||||
sendMessagePrice: string;
|
sendMessagePrice: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
welcomePlaceholder: string;
|
|
||||||
chatPlaceholder: string;
|
|
||||||
createSkillPrompt: string;
|
createSkillPrompt: string;
|
||||||
addAttachments: string;
|
addAttachments: string;
|
||||||
history: string;
|
history: string;
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@ export const zhCN: Translations = {
|
||||||
|
|
||||||
// Input Box
|
// Input Box
|
||||||
inputBox: {
|
inputBox: {
|
||||||
placeholder: "可直接对话; 或输入需求并选择skill,完成专业任务;",
|
placeholder: "可直接对话; 或输入需求并选择skill,完成专业任务;“@”可引用文件",
|
||||||
welcomePlaceholder: "可直接对话; 或输入需求并选择skill,完成专业任务。",
|
|
||||||
chatPlaceholder: "“@”可引用文件。",
|
|
||||||
createSkillPrompt:
|
createSkillPrompt:
|
||||||
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",
|
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",
|
||||||
sendMessagePrice:
|
sendMessagePrice:
|
||||||
|
|
@ -261,8 +259,7 @@ export const zhCN: Translations = {
|
||||||
noArtifactSelectedTitle: "未选择生成文件",
|
noArtifactSelectedTitle: "未选择生成文件",
|
||||||
noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
|
noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
|
||||||
exitDialogTitle: "提示",
|
exitDialogTitle: "提示",
|
||||||
exitDialogDescription:
|
exitDialogDescription: "历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
|
||||||
"历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
|
|
||||||
exitDialogConfirm: "确定",
|
exitDialogConfirm: "确定",
|
||||||
selectedSkillLoadFailed: "技能加载失败",
|
selectedSkillLoadFailed: "技能加载失败",
|
||||||
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import type { Model } from "./types";
|
||||||
|
|
||||||
export async function loadModels() {
|
export async function loadModels() {
|
||||||
const res = await fetch(`${getBackendBaseURL()}/api/models`);
|
const res = await fetch(`${getBackendBaseURL()}/api/models`);
|
||||||
|
|
||||||
if (res.status >= 500 && res.status < 600) {
|
if (res.status >= 500 && res.status < 600) {
|
||||||
throw new Error(`Server error: ${res.status}`);
|
throw new Error(`Server error: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP error: ${res.status}`);
|
throw new Error(`HTTP error: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { models } = (await res.json()) as { models: Model[] };
|
const { models } = (await res.json()) as { models: Model[] };
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ import type { UploadedFileInfo } from "../uploads";
|
||||||
import { listUploadedFiles, uploadFiles } from "../uploads";
|
import { listUploadedFiles, uploadFiles } from "../uploads";
|
||||||
import type { UploadTarget } from "../uploads/api";
|
import type { UploadTarget } from "../uploads/api";
|
||||||
|
|
||||||
import { buildPriorityHintText, composeSubmitText } from "./priority-hint";
|
|
||||||
import { buildFilesForSubmit } from "./submit-files";
|
import { buildFilesForSubmit } from "./submit-files";
|
||||||
|
import { buildPriorityHintText, composeSubmitText } from "./priority-hint";
|
||||||
import type {
|
import type {
|
||||||
AgentThread,
|
AgentThread,
|
||||||
AgentThreadContext,
|
AgentThreadContext,
|
||||||
|
|
@ -268,7 +268,8 @@ export function useThreadStream({
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastToast = lastErrorToastRef.current;
|
const lastToast = lastErrorToastRef.current;
|
||||||
if (
|
if (
|
||||||
lastToast?.message === message &&
|
lastToast &&
|
||||||
|
lastToast.message === message &&
|
||||||
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
|
now - lastToast.timestamp < STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import {
|
import {
|
||||||
POST_MESSAGE_TYPES,
|
POST_MESSAGE_TYPES,
|
||||||
RECEIVE_MESSAGE_TYPES,
|
RECEIVE_MESSAGE_TYPES,
|
||||||
|
|
@ -11,6 +10,7 @@ import {
|
||||||
type SelectedSkillPayloadItem,
|
type SelectedSkillPayloadItem,
|
||||||
sendToParent,
|
sendToParent,
|
||||||
} from "@/core/iframe-messages";
|
} from "@/core/iframe-messages";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||||
|
|
||||||
// Skill 数据类型
|
// Skill 数据类型
|
||||||
|
|
@ -39,20 +39,8 @@ function parseStoredSkills(raw: string | null): SkillData[] {
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (typeof item !== "object" || item === null) return null;
|
if (typeof item !== "object" || item === null) return null;
|
||||||
const record = item as Record<string, unknown>;
|
const record = item as Record<string, unknown>;
|
||||||
const rawSkillId = record.skill_id;
|
const skillId = String(record.skill_id ?? "").trim();
|
||||||
const skillId =
|
const title = String(record.title ?? "").trim();
|
||||||
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;
|
if (!skillId || !title) return null;
|
||||||
return { skill_id: skillId, title };
|
return { skill_id: skillId, title };
|
||||||
})
|
})
|
||||||
|
|
@ -96,11 +84,7 @@ export function useIframeSkill(
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const threadIdFromQuery = searchParams.get("thread_id");
|
const threadIdFromQuery = searchParams.get("thread_id");
|
||||||
const threadIdFromOptions = options?.threadId?.trim();
|
const threadId = options?.threadId?.trim() || threadIdFromQuery;
|
||||||
const threadId =
|
|
||||||
threadIdFromOptions && threadIdFromOptions.length > 0
|
|
||||||
? threadIdFromOptions
|
|
||||||
: threadIdFromQuery;
|
|
||||||
const isChattingFromQuery = searchParams.get("is_chatting");
|
const isChattingFromQuery = searchParams.get("is_chatting");
|
||||||
const lastThreadIdRef = useRef<string | null>(null);
|
const lastThreadIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -332,8 +316,7 @@ export function useIframeSkill(
|
||||||
setSelectedSkills(normalizedSkills);
|
setSelectedSkills(normalizedSkills);
|
||||||
|
|
||||||
toast.success(t.skills.loadSuccessWithTitle(title), {
|
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||||
description:
|
description: result.message || t.skills.createdFiles(result.created_files),
|
||||||
result.message || t.skills.createdFiles(result.created_files),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -342,9 +325,7 @@ export function useIframeSkill(
|
||||||
removeFailedSkills(failedIds);
|
removeFailedSkills(failedIds);
|
||||||
toast.dismiss("suggest-skill-bootstrap");
|
toast.dismiss("suggest-skill-bootstrap");
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : t.skills.networkRequestFailed;
|
||||||
? error.message
|
|
||||||
: t.skills.networkRequestFailed;
|
|
||||||
toast.error(t.skills.loadFailedWithTitle(title), {
|
toast.error(t.skills.loadFailedWithTitle(title), {
|
||||||
description: message,
|
description: message,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useCallback, useState, useRef } from "react";
|
import { useEffect, useCallback, useState, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
|
||||||
import {
|
import {
|
||||||
isSelectedSkillMessage,
|
isSelectedSkillMessage,
|
||||||
isSelectedSkillsMessage,
|
isSelectedSkillsMessage,
|
||||||
type SelectedSkillPayloadItem,
|
type SelectedSkillPayloadItem,
|
||||||
} from "@/core/iframe-messages";
|
} from "@/core/iframe-messages";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||||
|
|
||||||
/** 技能基础数据 */
|
/** 技能基础数据 */
|
||||||
|
|
@ -105,8 +105,7 @@ export function useSelectedSkillListener({
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
skillBootstrappedKeyRef.current = initKey;
|
skillBootstrappedKeyRef.current = initKey;
|
||||||
toast.success(t.skills.loadSuccessWithTitle(title), {
|
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||||
description:
|
description: result.message || t.skills.createdFiles(result.created_files),
|
||||||
result.message || t.skills.createdFiles(result.created_files),
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|
|
||||||
|
|
@ -77,32 +77,27 @@
|
||||||
"Segoe UI Symbol", "Noto Color Emoji";
|
"Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
|
||||||
--animate-fade-in: fade-in 1.1s;
|
--animate-fade-in: fade-in 1.1s;
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-fade-in-up: fade-in-up 0.15s ease-in-out forwards;
|
--animate-fade-in-up: fade-in-up 0.15s ease-in-out forwards;
|
||||||
|
|
||||||
@keyframes fade-in-up {
|
@keyframes fade-in-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(1rem) scale(1.2);
|
transform: translateY(1rem) scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-bouncing: bouncing 0.5s infinite alternate;
|
--animate-bouncing: bouncing 0.5s infinite alternate;
|
||||||
|
|
||||||
@keyframes bouncing {
|
@keyframes bouncing {
|
||||||
to {
|
to {
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
|
|
@ -111,13 +106,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-skeleton-entrance: skeleton-entrance 0.35s ease-out forwards;
|
--animate-skeleton-entrance: skeleton-entrance 0.35s ease-out forwards;
|
||||||
|
|
||||||
@keyframes skeleton-entrance {
|
@keyframes skeleton-entrance {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scaleX(0);
|
transform: scaleX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
|
|
@ -125,13 +118,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-suggestion-in: suggestion-in 0.2s ease-out forwards;
|
--animate-suggestion-in: suggestion-in 0.2s ease-out forwards;
|
||||||
|
|
||||||
@keyframes suggestion-in {
|
@keyframes suggestion-in {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-1.25rem);
|
transform: translateY(-1.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
@ -139,21 +130,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-wave: wave 0.6s ease-in-out 2;
|
--animate-wave: wave 0.6s ease-in-out 2;
|
||||||
|
|
||||||
@keyframes wave {
|
@keyframes wave {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
transform: rotate(20deg);
|
transform: rotate(20deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
transform: rotate(20deg);
|
transform: rotate(20deg);
|
||||||
}
|
}
|
||||||
|
|
@ -201,45 +188,36 @@
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-tooltip-background: var(--tooltip-background);
|
--color-tooltip-background: var(--tooltip-background);
|
||||||
--animate-aurora: aurora 8s ease-in-out infinite alternate;
|
--animate-aurora: aurora 8s ease-in-out infinite alternate;
|
||||||
|
|
||||||
@keyframes aurora {
|
@keyframes aurora {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
transform: rotate(-5deg) scale(0.9);
|
transform: rotate(-5deg) scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
background-position: 50% 100%;
|
background-position: 50% 100%;
|
||||||
transform: rotate(5deg) scale(1.1);
|
transform: rotate(5deg) scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 50%;
|
background-position: 100% 50%;
|
||||||
transform: rotate(-3deg) scale(0.95);
|
transform: rotate(-3deg) scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
background-position: 50% 0%;
|
background-position: 50% 0%;
|
||||||
transform: rotate(3deg) scale(1.05);
|
transform: rotate(3deg) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
transform: rotate(-5deg) scale(0.9);
|
transform: rotate(-5deg) scale(0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
--animate-shine: shine var(--duration) infinite linear;
|
--animate-shine: shine var(--duration) infinite linear;
|
||||||
|
|
||||||
@keyframes shine {
|
@keyframes shine {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0% 0%;
|
background-position: 0% 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 100%;
|
background-position: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
background-position: 0% 0%;
|
background-position: 0% 0%;
|
||||||
}
|
}
|
||||||
|
|
@ -330,27 +308,22 @@
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container-md {
|
.container-md {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@media (width >= 40rem) {
|
||||||
@media (width >=40rem) {
|
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
}
|
}
|
||||||
|
@media (width >= 48rem) {
|
||||||
@media (width >=48rem) {
|
|
||||||
max-width: 48rem;
|
max-width: 48rem;
|
||||||
}
|
}
|
||||||
|
@media (width >= 64rem) {
|
||||||
@media (width >=64rem) {
|
|
||||||
max-width: 64rem;
|
max-width: 64rem;
|
||||||
}
|
}
|
||||||
|
@media (width >= 80rem) {
|
||||||
@media (width >=80rem) {
|
|
||||||
max-width: 80rem;
|
max-width: 80rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -402,11 +375,9 @@
|
||||||
0% {
|
0% {
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 400% 0;
|
background-position: 400% 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
}
|
}
|
||||||
|
|
@ -427,14 +398,13 @@
|
||||||
|
|
||||||
/* Hide scrollbar but keep scroll behavior */
|
/* Hide scrollbar but keep scroll behavior */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome, Safari, Opera */
|
/* Chrome, Safari, Opera */
|
||||||
*::-webkit-scrollbar {
|
/* *::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
} */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--container-width-xs: calc(var(--spacing) * 72);
|
--container-width-xs: calc(var(--spacing) * 72);
|
||||||
|
|
@ -465,7 +435,6 @@ body {
|
||||||
p {
|
p {
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 特别指定,代码块和正文一样的字体 */
|
/* 特别指定,代码块和正文一样的字体 */
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
|
|
@ -474,9 +443,8 @@ pre {
|
||||||
font-family:
|
font-family:
|
||||||
"Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
|
"Microsoft YaHei", "微软雅黑", "PingFang SC", sans-serif !important;
|
||||||
}
|
}
|
||||||
|
pre{
|
||||||
pre {
|
border-radius: 5px;
|
||||||
border-radius: 5px;
|
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -494,14 +462,12 @@ pre {
|
||||||
|
|
||||||
/* 二三级标题 - 16px */
|
/* 二三级标题 - 16px */
|
||||||
[data-streamdown="heading-2"],
|
[data-streamdown="heading-2"],
|
||||||
[data-streamdown="heading-3"],
|
[data-streamdown="heading-3"],[data-streamdown="heading-4"] {
|
||||||
[data-streamdown="heading-4"] {
|
|
||||||
font-size: calc(16px * var(--zoom-scale));
|
font-size: calc(16px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 代码块 - 14px */
|
/* 代码块 - 14px */
|
||||||
[data-streamdown="code-block"] pre,
|
[data-streamdown="code-block"] pre,code {
|
||||||
code {
|
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -516,69 +482,56 @@ code {
|
||||||
[data-streamdown="table-cell"] {
|
[data-streamdown="table-cell"] {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
height: calc(42px * var(--zoom-scale));
|
height:calc(42px * var(--zoom-scale)) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-streamdown="table-header"] {
|
[data-streamdown="table-header"] {
|
||||||
background: #9c9b9b26;
|
background: #9c9b9b26;
|
||||||
height: calc(50px * var(--zoom-scale));
|
height: calc(50px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-streamdown="table-header"] th {
|
[data-streamdown="table-header"] th {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
|
}
|
||||||
|
[data-slot="hover-card-trigger"] [data-slot="badge"]{
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="hover-card-trigger"] [data-slot="badge"] {
|
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格四角圆角:由四个角单元格承担视觉圆角 */
|
/* 表格四角圆角:由四个角单元格承担视觉圆角 */
|
||||||
[data-streamdown="table-header"]
|
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:first-child {
|
||||||
tr:first-child
|
|
||||||
> [data-streamdown="table-header-cell"]:first-child {
|
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-streamdown="table-header"]
|
[data-streamdown="table-header"] tr:first-child > [data-streamdown="table-header-cell"]:last-child {
|
||||||
tr:first-child
|
|
||||||
> [data-streamdown="table-header-cell"]:last-child {
|
|
||||||
border-top-right-radius: 5px;
|
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));
|
padding-top: calc(20px * var(--zoom-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 行分隔线 */
|
/* 行分隔线 */
|
||||||
/* [data-streamdown="table-body"] tr {
|
[data-streamdown="table-body"] tr{
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid var(--border);
|
||||||
} */
|
}
|
||||||
|
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:first-child {
|
||||||
[data-streamdown="table-body"]
|
|
||||||
tr:last-child
|
|
||||||
> [data-streamdown="table-cell"]:first-child {
|
|
||||||
border-bottom-left-radius: 5px;
|
border-bottom-left-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-streamdown="table-body"]
|
[data-streamdown="table-body"] tr:last-child > [data-streamdown="table-cell"]:last-child {
|
||||||
tr:last-child
|
|
||||||
> [data-streamdown="table-cell"]:last-child {
|
|
||||||
border-bottom-right-radius: 5px;
|
border-bottom-right-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-streamdown="table-body"] tr:last-child td {
|
[data-streamdown="table-body"] tr:last-child {
|
||||||
line-height: calc(14px * var(--zoom-scale));
|
padding-top: calc(50px * var(--zoom-scale));
|
||||||
padding-bottom: calc(20px * var(--zoom-scale));
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-streamdown="table-row"] > [data-streamdown="table-cell"] {
|
}
|
||||||
line-height: calc(42px * var(--zoom-scale));
|
[data-streamdown="table-row"] >[data-streamdown="table-cell"]{
|
||||||
vertical-align: top;
|
line-height: 14px;
|
||||||
|
vertical-align: top;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.cm-line {
|
.cm-line {
|
||||||
font-size: calc(14px * var(--zoom-scale));
|
font-size: calc(14px * var(--zoom-scale));
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
@ -615,7 +568,3 @@ code {
|
||||||
.pptx-preview-wrap .pptx-preview-wrapper {
|
.pptx-preview-wrap .pptx-preview-wrapper {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker-char{
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export async function rewriteFirstReferenceAsArtifact(
|
||||||
return false;
|
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;
|
return?: unknown;
|
||||||
memoizedState?: unknown;
|
memoizedState?: unknown;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { newChatEntry, openChat, sendMessage } from "./support/chat-helpers";
|
||||||
|
|
||||||
function logProgress(message: string) {
|
function logProgress(message: string) {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(`[DF-SEC][${timestamp}] ${message}`);
|
console.log(`[DF-SEC][${timestamp}] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +21,10 @@ function parseForbiddenPrefixes() {
|
||||||
return prefixes;
|
return prefixes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertNoForbiddenPrefixOnScreen(page: Page, prefixes: string[]) {
|
async function assertNoForbiddenPrefixOnScreen(
|
||||||
|
page: Page,
|
||||||
|
prefixes: string[],
|
||||||
|
) {
|
||||||
if (prefixes.length === 0) return;
|
if (prefixes.length === 0) return;
|
||||||
const leaked = await page.evaluate((items) => {
|
const leaked = await page.evaluate((items) => {
|
||||||
const text = document.body?.innerText ?? "";
|
const text = document.body?.innerText ?? "";
|
||||||
|
|
@ -61,7 +64,9 @@ async function waitForConditionWithLeakCheck({
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastLogAt >= logEveryMs) {
|
if (now - lastLogAt >= logEveryMs) {
|
||||||
lastLogAt = now;
|
lastLogAt = now;
|
||||||
logProgress(`${label}… (${Math.round((now - start) / 1000)}s elapsed)`);
|
logProgress(
|
||||||
|
`${label}… (${Math.round((now - start) / 1000)}s elapsed)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await page.waitForTimeout(stepMs);
|
await page.waitForTimeout(stepMs);
|
||||||
|
|
@ -108,10 +113,7 @@ async function waitForArtifactCards({
|
||||||
label,
|
label,
|
||||||
condition: async () => {
|
condition: async () => {
|
||||||
// Cards only render when the panel is open. Try to open opportunistically.
|
// Cards only render when the panel is open. Try to open opportunistically.
|
||||||
if (
|
if ((await fileList.count()) === 0 || !(await fileList.first().isVisible())) {
|
||||||
(await fileList.count()) === 0 ||
|
|
||||||
!(await fileList.first().isVisible())
|
|
||||||
) {
|
|
||||||
await openArtifactsPanelIfPossible(page);
|
await openArtifactsPanelIfPossible(page);
|
||||||
}
|
}
|
||||||
if ((await cards.count()) < minCount) return false;
|
if ((await cards.count()) < minCount) return false;
|
||||||
|
|
@ -167,7 +169,11 @@ async function sendMessageSafely({
|
||||||
});
|
});
|
||||||
await textarea.evaluate((element) => {
|
await textarea.evaluate((element) => {
|
||||||
const target = element as HTMLTextAreaElement;
|
const target = element as HTMLTextAreaElement;
|
||||||
target.value = "";
|
const setter = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLTextAreaElement.prototype,
|
||||||
|
"value",
|
||||||
|
)?.set;
|
||||||
|
setter?.call(target, "");
|
||||||
target.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
target.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
||||||
});
|
});
|
||||||
await page.keyboard.insertText(text);
|
await page.keyboard.insertText(text);
|
||||||
|
|
@ -235,8 +241,7 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
||||||
timeoutMs: 40_000,
|
timeoutMs: 40_000,
|
||||||
label: "Wait for steps signal",
|
label: "Wait for steps signal",
|
||||||
condition: async () =>
|
condition: async () =>
|
||||||
(await stepsSignal.count()) > 0 &&
|
(await stepsSignal.count()) > 0 && (await stepsSignal.first().isVisible()),
|
||||||
(await stepsSignal.first().isVisible()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按需求:40s 内未出现思考块则中断后续检查(标记为 skip)。
|
// 按需求:40s 内未出现思考块则中断后续检查(标记为 skip)。
|
||||||
|
|
@ -251,10 +256,9 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
||||||
minCount: 1,
|
minCount: 1,
|
||||||
label: "Wait for first artifact card",
|
label: "Wait for first artifact card",
|
||||||
});
|
});
|
||||||
expect(
|
expect(firstArtifacts.ok, "未检测到 artifact-file-card,图片可能未生成完成").toBe(
|
||||||
firstArtifacts.ok,
|
true,
|
||||||
"未检测到 artifact-file-card,图片可能未生成完成",
|
);
|
||||||
).toBe(true);
|
|
||||||
logProgress(
|
logProgress(
|
||||||
`First artifact ready (count=${await firstArtifacts.cards.count()}).`,
|
`First artifact ready (count=${await firstArtifacts.cards.count()}).`,
|
||||||
);
|
);
|
||||||
|
|
@ -275,10 +279,7 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
|
||||||
minCount: beforeSecondCount + 1,
|
minCount: beforeSecondCount + 1,
|
||||||
label: "Wait for second artifact card",
|
label: "Wait for second artifact card",
|
||||||
});
|
});
|
||||||
expect(
|
expect(secondArtifacts.ok, "未检测到新的产物生成(artifact 数量未增加)").toBe(true);
|
||||||
secondArtifacts.ok,
|
|
||||||
"未检测到新的产物生成(artifact 数量未增加)",
|
|
||||||
).toBe(true);
|
|
||||||
logProgress(
|
logProgress(
|
||||||
`Second artifact ready (count=${await secondArtifacts.cards.count()}).`,
|
`Second artifact ready (count=${await secondArtifacts.cards.count()}).`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<!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