diff --git a/apps/dashboard/messages/en.json b/apps/dashboard/messages/en.json index af2b9c0c..0e418f8c 100644 --- a/apps/dashboard/messages/en.json +++ b/apps/dashboard/messages/en.json @@ -412,6 +412,11 @@ "loadingHistory": "Loading history…", "toolRunning": "running", "toolDone": "done", + "toolPending": "pending", + "toolError": "error", + "toolDenied": "denied", + "toolAwaitingApproval": "awaiting approval", + "toolResponded": "responded", "toolResult": "output", "errorGeneric": "Agent reply failed. Please retry — likely an upstream LLM error or dropped stream.", "errorDismiss": "Dismiss", diff --git a/apps/dashboard/messages/zh.json b/apps/dashboard/messages/zh.json index 81c2db03..8c587110 100644 --- a/apps/dashboard/messages/zh.json +++ b/apps/dashboard/messages/zh.json @@ -412,6 +412,11 @@ "loadingHistory": "加载历史会话…", "toolRunning": "调用中", "toolDone": "已完成", + "toolPending": "待处理", + "toolError": "出错", + "toolDenied": "已拒绝", + "toolAwaitingApproval": "待审批", + "toolResponded": "已响应", "toolResult": "输出", "errorGeneric": "Agent 回复失败,请重试 —— 多半是上游 LLM 报错或连接中断。", "errorDismiss": "关闭", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index b079dc10..fa80046b 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -20,6 +20,8 @@ "@copilotkit/react-core": "^1.59.5", "@copilotkit/runtime": "^1.59.5", "@mastra/client-js": "^1.23.2", + "@streamdown/cjk": "^1.0.3", + "@streamdown/code": "^1.1.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "geist": "^1.7.1", @@ -32,9 +34,9 @@ "openai": "^6.42.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "server-only": "^0.0.1", + "streamdown": "^2.5.0", "swr": "^2.3.6", "tailwind-merge": "^3.6.0" }, diff --git a/apps/dashboard/pnpm-lock.yaml b/apps/dashboard/pnpm-lock.yaml index 88e7e230..a02dedfc 100644 --- a/apps/dashboard/pnpm-lock.yaml +++ b/apps/dashboard/pnpm-lock.yaml @@ -24,6 +24,12 @@ importers: '@mastra/client-js': specifier: ^1.23.2 version: 1.23.2(@bufbuild/protobuf@2.12.0)(@cfworker/json-schema@4.1.1)(ai@6.0.196(zod@4.4.3))(express@5.2.1)(rxjs@7.8.1)(zod@4.4.3) + '@streamdown/cjk': + specifier: ^1.0.3 + version: 1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7)(unified@11.0.5) + '@streamdown/code': + specifier: ^1.1.1 + version: 1.1.1(react@19.2.7) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -60,15 +66,15 @@ importers: react-dom: specifier: ^19.2.6 version: 19.2.7(react@19.2.7) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.16)(react@19.2.7) remark-gfm: specifier: ^4.0.1 version: 4.0.1 server-only: specifier: ^0.0.1 version: 0.0.1 + streamdown: + specifier: ^2.5.0 + version: 2.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) swr: specifier: ^2.3.6 version: 2.4.1(react@19.2.7) @@ -1255,6 +1261,16 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@streamdown/cjk@1.0.3': + resolution: {integrity: sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/code@1.1.1': + resolution: {integrity: sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + '@swc/core-darwin-arm64@1.15.40': resolution: {integrity: sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==} engines: {node: '>=10'} @@ -2792,6 +2808,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2847,6 +2868,24 @@ packages: mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown-cjk-friendly-gfm-strikethrough@1.0.0: + resolution: {integrity: sha512-1ePVfB4P/vz3xSsm6H3D32r6VYGErxclnuLLFK02/2ReF+UdEKm7caulK6Vm0LBIp5gPRtB2Z1OYDznCkX3k2w==} + engines: {node: '>=18'} + peerDependencies: + '@types/mdast': '*' + peerDependenciesMeta: + '@types/mdast': + optional: true + + mdast-util-to-markdown-cjk-friendly@1.0.0: + resolution: {integrity: sha512-BoaAm8mlJ+LAYz0Qs532Y3ciTuQYgBUPZcSFbvC/ZKmEMAKgulw84YvQK1gI34t/vL2euSfuaWlqczkTBgamkw==} + engines: {node: '>=18'} + peerDependencies: + '@types/mdast': '*' + peerDependenciesMeta: + '@types/mdast': + optional: true + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -2894,6 +2933,16 @@ packages: micromark-util-types: optional: true + micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1: + resolution: {integrity: sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg==} + engines: {node: '>=18'} + peerDependencies: + micromark: ^4.0.0 + micromark-util-types: ^2.0.0 + peerDependenciesMeta: + micromark-util-types: + optional: true + micromark-extension-cjk-friendly-util@2.1.1: resolution: {integrity: sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg==} engines: {node: '>=16'} @@ -2903,6 +2952,15 @@ packages: micromark-util-types: optional: true + micromark-extension-cjk-friendly-util@3.0.1: + resolution: {integrity: sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w==} + engines: {node: '>=18'} + peerDependencies: + micromark-util-types: '*' + peerDependenciesMeta: + micromark-util-types: + optional: true + micromark-extension-cjk-friendly@1.2.3: resolution: {integrity: sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q==} engines: {node: '>=16'} @@ -2913,6 +2971,16 @@ packages: micromark-util-types: optional: true + micromark-extension-cjk-friendly@2.0.1: + resolution: {integrity: sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw==} + engines: {node: '>=18'} + peerDependencies: + micromark: ^4.0.0 + micromark-util-types: ^2.0.0 + peerDependenciesMeta: + micromark-util-types: + optional: true + micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -3399,12 +3467,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-markdown@10.1.0: - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - react-markdown@8.0.7: resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} peerDependencies: @@ -3487,6 +3549,16 @@ packages: '@types/mdast': optional: true + remark-cjk-friendly-gfm-strikethrough@2.3.1: + resolution: {integrity: sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==} + engines: {node: '>=18'} + peerDependencies: + '@types/mdast': ^4.0.0 + unified: ^11.0.0 + peerDependenciesMeta: + '@types/mdast': + optional: true + remark-cjk-friendly@1.2.3: resolution: {integrity: sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g==} engines: {node: '>=16'} @@ -3497,6 +3569,16 @@ packages: '@types/mdast': optional: true + remark-cjk-friendly@2.3.1: + resolution: {integrity: sha512-f+pKZRxCRwNEGFBKNRAZAqU91GIK1SAo3ZyFHWRUgC9zcxRR0BXKd6YwqgSsxtW0rNpUDtONj7H5nje2WL3fcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/mdast': ^4.0.0 + unified: ^11.0.0 + peerDependenciesMeta: + '@types/mdast': + optional: true + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -3665,6 +3747,12 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + streamdown@2.5.0: + resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -5313,6 +5401,24 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@streamdown/cjk@1.0.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7)(unified@11.0.5)': + dependencies: + react: 19.2.7 + remark-cjk-friendly: 2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) + remark-cjk-friendly-gfm-strikethrough: 2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) + unist-util-visit: 5.1.0 + transitivePeerDependencies: + - '@types/mdast' + - micromark + - micromark-util-types + - supports-color + - unified + + '@streamdown/code@1.1.1(react@19.2.7)': + dependencies: + react: 19.2.7 + shiki: 3.23.0 + '@swc/core-darwin-arm64@1.15.40': optional: true @@ -6905,6 +7011,8 @@ snapshots: marked@16.4.2: {} + marked@17.0.6: {} + math-intrinsics@1.1.0: {} mdast-util-definitions@5.1.2: @@ -7090,6 +7198,28 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + mdast-util-to-markdown-cjk-friendly-gfm-strikethrough@1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2): + dependencies: + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-to-markdown: 2.1.2 + micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) + micromark-util-symbol: 2.0.1 + optionalDependencies: + '@types/mdast': 4.0.4 + transitivePeerDependencies: + - micromark-util-types + - supports-color + + mdast-util-to-markdown-cjk-friendly@1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2): + dependencies: + mdast-util-to-markdown: 2.1.2 + micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) + micromark-util-symbol: 2.0.1 + optionalDependencies: + '@types/mdast': 4.0.4 + transitivePeerDependencies: + - micromark-util-types + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -7195,6 +7325,19 @@ snapshots: optionalDependencies: micromark-util-types: 2.0.2 + micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): + dependencies: + devlop: 1.1.0 + get-east-asian-width: 1.6.0 + micromark: 4.0.2 + micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + optionalDependencies: + micromark-util-types: 2.0.2 + micromark-extension-cjk-friendly-util@2.1.1(micromark-util-types@2.0.2): dependencies: get-east-asian-width: 1.6.0 @@ -7203,6 +7346,14 @@ snapshots: optionalDependencies: micromark-util-types: 2.0.2 + micromark-extension-cjk-friendly-util@3.0.1(micromark-util-types@2.0.2): + dependencies: + get-east-asian-width: 1.6.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + optionalDependencies: + micromark-util-types: 2.0.2 + micromark-extension-cjk-friendly@1.2.3(micromark-util-types@2.0.2)(micromark@4.0.2): dependencies: devlop: 1.1.0 @@ -7214,6 +7365,17 @@ snapshots: optionalDependencies: micromark-util-types: 2.0.2 + micromark-extension-cjk-friendly@2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2): + dependencies: + devlop: 1.1.0 + micromark: 4.0.2 + micromark-extension-cjk-friendly-util: 3.0.1(micromark-util-types@2.0.2) + micromark-util-chunked: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + optionalDependencies: + micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.1 @@ -7831,24 +7993,6 @@ snapshots: react-is@18.3.1: {} - react-markdown@10.1.0(@types/react@19.2.16)(react@19.2.7): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.2.16 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.1 - react: 19.2.7 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - react-markdown@8.0.7(@types/react@19.2.16)(react@19.2.7): dependencies: '@types/hast': 2.3.10 @@ -7957,6 +8101,18 @@ snapshots: - micromark - micromark-util-types + remark-cjk-friendly-gfm-strikethrough@2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): + dependencies: + mdast-util-to-markdown-cjk-friendly-gfm-strikethrough: 1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2) + micromark-extension-cjk-friendly-gfm-strikethrough: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) + unified: 11.0.5 + optionalDependencies: + '@types/mdast': 4.0.4 + transitivePeerDependencies: + - micromark + - micromark-util-types + - supports-color + remark-cjk-friendly@1.2.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): dependencies: micromark-extension-cjk-friendly: 1.2.3(micromark-util-types@2.0.2)(micromark@4.0.2) @@ -7967,6 +8123,17 @@ snapshots: - micromark - micromark-util-types + remark-cjk-friendly@2.3.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5): + dependencies: + mdast-util-to-markdown-cjk-friendly: 1.0.0(@types/mdast@4.0.4)(micromark-util-types@2.0.2) + micromark-extension-cjk-friendly: 2.0.1(micromark-util-types@2.0.2)(micromark@4.0.2) + unified: 11.0.5 + optionalDependencies: + '@types/mdast': 4.0.4 + transitivePeerDependencies: + - micromark + - micromark-util-types + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -8263,6 +8430,29 @@ snapshots: - micromark-util-types - supports-color + streamdown@2.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + clsx: 2.1.1 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + marked: 17.0.6 + mermaid: 11.15.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + rehype-harden: 1.1.8 + rehype-raw: 7.0.0 + rehype-sanitize: 6.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remend: 1.3.0 + tailwind-merge: 3.6.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 diff --git a/apps/dashboard/src/components/chat/ChatErrorBanner.tsx b/apps/dashboard/src/components/chat/ChatErrorBanner.tsx new file mode 100644 index 00000000..5b297cb8 --- /dev/null +++ b/apps/dashboard/src/components/chat/ChatErrorBanner.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { TriangleAlert, X } from "lucide-react"; + +/** + * Agent 错误横幅。 + * + * 上游 LLM 报错 / 流中断时在对话栏顶部显示红字提示, + * 用于区分"agent 在思考"和"真的出错了"。 + */ +export function ChatErrorBanner({ + error, + onDismiss, + dismissLabel, +}: { + error: string; + onDismiss: () => void; + dismissLabel: string; +}) { + return ( +
+ + {error} + +
+ ); +} diff --git a/apps/dashboard/src/components/chat/ChatHistoryPanel.tsx b/apps/dashboard/src/components/chat/ChatHistoryPanel.tsx new file mode 100644 index 00000000..2500ad04 --- /dev/null +++ b/apps/dashboard/src/components/chat/ChatHistoryPanel.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { cn } from "@/lib/cn"; + +/** 历史会话摘要。 */ +export interface ThreadSummary { + id: string; + title: string | null; + updatedAt: string; +} + +/** + * 历史会话下拉面板。 + * + * 纯展示组件,状态由父组件(ChatThread)管理。 + */ +export function ChatHistoryPanel({ + open, + threads, + historyError, + currentThreadId, + sourceDownLabel, + untitledLabel, + onSwitch, + onReload, +}: { + open: boolean; + threads: ThreadSummary[] | null; + historyError: boolean; + currentThreadId: string; + sourceDownLabel: string; + untitledLabel: string; + onSwitch: (threadId: string) => void; + onReload: (threadId: string) => void; +}) { + const t = useTranslations("chat"); + + if (!open) return null; + + return ( +
+ {threads === null ? ( +

+ {t("loadingHistory")} +

+ ) : threads.length === 0 ? ( +

+ {t("historyEmpty")} +

+ ) : ( + threads.map((th) => ( + + )) + )} + {historyError && ( +

+ {sourceDownLabel} +

+ )} +
+ ); +} diff --git a/apps/dashboard/src/components/chat/ChatInput.tsx b/apps/dashboard/src/components/chat/ChatInput.tsx new file mode 100644 index 00000000..f5265c74 --- /dev/null +++ b/apps/dashboard/src/components/chat/ChatInput.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { MapPin, SendHorizontal, Square, X } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { type KeyboardEvent, useCallback, useEffect, useRef } from "react"; + +import { cn } from "@/lib/cn"; + +/** + * 对话输入区域:输入框 + 发送/停止按钮 + 页面上下文胶囊。 + * + * 所有状态由父组件(ChatThread)管理,通过 props 传入。 + * + * 聚焦:`open` 由父组件传入——对话栏由 `translate-x` 滑入而非条件卸载, + * 所以"打开时聚焦输入框"必须由 open prop 变化驱动(组件挂载 effect 补不上)。 + */ +export function ChatInput({ + draft, + isLoading, + open, + contextAttached, + contextKind, + contextId, + onDraftChange, + onSubmit, + onStop, + onContextDismiss, +}: { + draft: string; + isLoading: boolean; + open: boolean; + contextAttached: boolean; + contextKind: string; + contextId?: string; + onDraftChange: (v: string) => void; + onSubmit: () => void; + onStop: () => void; + onContextDismiss: () => void; +}) { + const t = useTranslations("chat"); + const textareaRef = useRef(null); + + // 打开对话栏时聚焦输入框(Phase 3 拆分后从 ChatThread 迁移进来)。 + useEffect(() => { + if (open) textareaRef.current?.focus(); + }, [open]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + onSubmit(); + } + }, + [onSubmit], + ); + + return ( +
+ {contextAttached && ( +
+ + + {t(`context.kind.${contextKind}`)} + + {contextId && ( + + {contextId.slice(0, 8)} + + )} + +
+ )} +
+