@huyooo/ai-chat-frontend-react 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +368 -0
  2. package/dist/index.css +2575 -0
  3. package/dist/index.css.map +1 -0
  4. package/dist/index.d.ts +378 -135
  5. package/dist/index.js +3956 -1042
  6. package/dist/index.js.map +1 -1
  7. package/dist/style.css +48 -987
  8. package/package.json +7 -4
  9. package/src/adapter.ts +10 -70
  10. package/src/components/ChatPanel.tsx +373 -117
  11. package/src/components/common/ConfirmDialog.css +136 -0
  12. package/src/components/common/ConfirmDialog.tsx +91 -0
  13. package/src/components/common/CopyButton.css +22 -0
  14. package/src/components/common/CopyButton.tsx +46 -0
  15. package/src/components/common/IndexingSettings.css +207 -0
  16. package/src/components/common/IndexingSettings.tsx +398 -0
  17. package/src/components/common/SettingsPanel.css +256 -0
  18. package/src/components/common/SettingsPanel.tsx +120 -0
  19. package/src/components/common/Toast.css +50 -0
  20. package/src/components/common/Toast.tsx +38 -0
  21. package/src/components/common/ToggleSwitch.css +52 -0
  22. package/src/components/common/ToggleSwitch.tsx +20 -0
  23. package/src/components/header/ChatHeader.css +285 -0
  24. package/src/components/header/ChatHeader.tsx +376 -0
  25. package/src/components/input/AtFilePicker.css +147 -0
  26. package/src/components/input/AtFilePicker.tsx +519 -0
  27. package/src/components/input/ChatInput.css +204 -0
  28. package/src/components/input/ChatInput.tsx +506 -0
  29. package/src/components/input/DropdownSelector.css +159 -0
  30. package/src/components/input/DropdownSelector.tsx +195 -0
  31. package/src/components/input/ImagePreviewModal.css +124 -0
  32. package/src/components/input/ImagePreviewModal.tsx +118 -0
  33. package/src/components/input/at-views/AtBranchView.tsx +34 -0
  34. package/src/components/input/at-views/AtBrowserView.tsx +34 -0
  35. package/src/components/input/at-views/AtChatsView.tsx +34 -0
  36. package/src/components/input/at-views/AtDocsView.tsx +34 -0
  37. package/src/components/input/at-views/AtFilesView.tsx +168 -0
  38. package/src/components/input/at-views/AtTerminalsView.tsx +34 -0
  39. package/src/components/input/at-views/AtViewStyles.css +143 -0
  40. package/src/components/input/at-views/index.ts +9 -0
  41. package/src/components/message/ContentRenderer.css +9 -0
  42. package/src/components/message/ContentRenderer.tsx +63 -0
  43. package/src/components/message/MessageBubble.css +190 -0
  44. package/src/components/message/MessageBubble.tsx +231 -0
  45. package/src/components/message/PartsRenderer.css +4 -0
  46. package/src/components/message/PartsRenderer.tsx +114 -0
  47. package/src/components/message/ToolResultRenderer.tsx +21 -0
  48. package/src/components/message/WelcomeMessage.css +221 -0
  49. package/src/components/message/WelcomeMessage.tsx +93 -0
  50. package/src/components/message/blocks/CodeBlock.tsx +60 -0
  51. package/src/components/message/blocks/TextBlock.tsx +15 -0
  52. package/src/components/message/blocks/blocks.css +141 -0
  53. package/src/components/message/blocks/index.ts +6 -0
  54. package/src/components/message/parts/CollapsibleCard.css +78 -0
  55. package/src/components/message/parts/CollapsibleCard.tsx +77 -0
  56. package/src/components/message/parts/ErrorPart.css +9 -0
  57. package/src/components/message/parts/ErrorPart.tsx +40 -0
  58. package/src/components/message/parts/ImagePart.css +50 -0
  59. package/src/components/message/parts/ImagePart.tsx +54 -0
  60. package/src/components/message/parts/SearchPart.css +44 -0
  61. package/src/components/message/parts/SearchPart.tsx +63 -0
  62. package/src/components/message/parts/TextPart.css +10 -0
  63. package/src/components/message/parts/TextPart.tsx +20 -0
  64. package/src/components/message/parts/ThinkingPart.css +9 -0
  65. package/src/components/message/parts/ThinkingPart.tsx +48 -0
  66. package/src/components/message/parts/ToolCallPart.css +220 -0
  67. package/src/components/message/parts/ToolCallPart.tsx +285 -0
  68. package/src/components/message/parts/ToolResultPart.css +68 -0
  69. package/src/components/message/parts/ToolResultPart.tsx +96 -0
  70. package/src/components/message/parts/index.ts +11 -0
  71. package/src/components/message/tool-results/DefaultToolResult.tsx +26 -0
  72. package/src/components/message/tool-results/SearchResults.tsx +69 -0
  73. package/src/components/message/tool-results/WeatherCard.tsx +63 -0
  74. package/src/components/message/tool-results/index.ts +7 -0
  75. package/src/components/message/tool-results/tool-results.css +179 -0
  76. package/src/components/message/welcome-types.ts +46 -0
  77. package/src/context/AutoRunConfigContext.tsx +13 -0
  78. package/src/context/ChatAdapterContext.tsx +8 -0
  79. package/src/context/ChatInputContext.tsx +40 -0
  80. package/src/context/RenderersContext.tsx +41 -0
  81. package/src/hooks/useChat.ts +855 -237
  82. package/src/hooks/useImageUpload.ts +253 -0
  83. package/src/index.ts +96 -39
  84. package/src/styles.css +48 -987
  85. package/src/types/index.ts +172 -103
  86. package/src/utils/fileIcon.ts +49 -0
  87. package/src/components/ChatInput.tsx +0 -368
  88. package/src/components/chat/messages/ExecutionSteps.tsx +0 -234
  89. package/src/components/chat/messages/MessageBubble.tsx +0 -130
  90. package/src/components/chat/ui/ChatHeader.tsx +0 -301
  91. package/src/components/chat/ui/WelcomeMessage.tsx +0 -107
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 代码块组件
3
+ */
4
+
5
+ import { FC, useState, useCallback, useMemo } from 'react'
6
+ import { Icon } from '@iconify/react'
7
+ import { highlightCode, getLanguageDisplayName } from '@huyooo/ai-chat-shared'
8
+ import type { CodeBlock as CodeBlockType } from '@huyooo/ai-chat-shared'
9
+ import './blocks.css'
10
+
11
+ interface CodeBlockProps {
12
+ block: CodeBlockType
13
+ onCopy?: (code: string) => void
14
+ }
15
+
16
+ export const CodeBlock: FC<CodeBlockProps> = ({ block, onCopy }) => {
17
+ const [copied, setCopied] = useState(false)
18
+
19
+ // 语言显示名称
20
+ const languageDisplay = useMemo(
21
+ () => getLanguageDisplayName(block.language),
22
+ [block.language]
23
+ )
24
+
25
+ // 高亮后的代码
26
+ const highlightedCode = useMemo(
27
+ () => highlightCode(block.content, block.language),
28
+ [block.content, block.language]
29
+ )
30
+
31
+ /** 复制代码 */
32
+ const handleCopy = useCallback(async () => {
33
+ try {
34
+ await navigator.clipboard.writeText(block.content)
35
+ setCopied(true)
36
+ onCopy?.(block.content)
37
+ setTimeout(() => setCopied(false), 2000)
38
+ } catch (err) {
39
+ console.error('复制失败:', err)
40
+ }
41
+ }, [block.content, onCopy])
42
+
43
+ return (
44
+ <div className="code-block">
45
+ {/* 头部:语言 + 操作按钮 */}
46
+ <div className="code-header">
47
+ <span className="code-language">{languageDisplay}</span>
48
+ <div className="code-actions">
49
+ <button className="code-action-btn" title="复制代码" onClick={handleCopy}>
50
+ <Icon icon={copied ? 'lucide:check' : 'lucide:copy'} width={14} />
51
+ </button>
52
+ </div>
53
+ </div>
54
+ {/* 代码内容 */}
55
+ <pre className="code-content">
56
+ <code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
57
+ </pre>
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 文本块组件
3
+ */
4
+
5
+ import { FC } from 'react'
6
+ import type { TextBlock as TextBlockType } from '@huyooo/ai-chat-shared'
7
+ import './blocks.css'
8
+
9
+ interface TextBlockProps {
10
+ block: TextBlockType
11
+ }
12
+
13
+ export const TextBlock: FC<TextBlockProps> = ({ block }) => {
14
+ return <div className="text-block">{block.content}</div>
15
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * 内容块共享样式
3
+ */
4
+
5
+ .text-block {
6
+ font-size: 14px;
7
+ line-height: 1.7;
8
+ color: var(--chat-text, #ccc);
9
+ white-space: pre-wrap;
10
+ word-break: break-word;
11
+ }
12
+
13
+ .code-block {
14
+ background: var(--chat-code-bg, #1f2937);
15
+ border-radius: 8px;
16
+ overflow: hidden;
17
+ margin: 8px 0;
18
+ }
19
+
20
+ .code-header {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ padding: 8px 12px;
25
+ background: rgba(0, 0, 0, 0.2);
26
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
27
+ }
28
+
29
+ .code-language {
30
+ font-size: 12px;
31
+ color: var(--chat-text-muted, #888);
32
+ font-family: "SF Mono", Monaco, monospace;
33
+ }
34
+
35
+ .code-actions {
36
+ display: flex;
37
+ gap: 4px;
38
+ }
39
+
40
+ .code-action-btn {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 28px;
45
+ height: 28px;
46
+ border: none;
47
+ background: transparent;
48
+ border-radius: 4px;
49
+ color: var(--chat-text-muted, #888);
50
+ cursor: pointer;
51
+ transition: all 0.15s;
52
+ }
53
+
54
+ .code-action-btn:hover {
55
+ background: rgba(255, 255, 255, 0.1);
56
+ color: var(--chat-text, #ccc);
57
+ }
58
+
59
+ .code-content {
60
+ margin: 0;
61
+ padding: 12px;
62
+ overflow-x: auto;
63
+ font-family: "SF Mono", Monaco, Consolas, monospace;
64
+ font-size: 13px;
65
+ line-height: 1.5;
66
+ color: var(--chat-code-text, #e5e7eb);
67
+ }
68
+
69
+ /* 统一滚动条样式 */
70
+ .code-content::-webkit-scrollbar {
71
+ width: 6px;
72
+ height: 6px;
73
+ }
74
+
75
+ .code-content::-webkit-scrollbar-track {
76
+ background: transparent;
77
+ }
78
+
79
+ .code-content::-webkit-scrollbar-thumb {
80
+ background: var(--chat-scrollbar, rgba(255, 255, 255, 0.2));
81
+ border-radius: 3px;
82
+ }
83
+
84
+ .code-content::-webkit-scrollbar-thumb:hover {
85
+ background: var(--chat-scrollbar-hover, rgba(255, 255, 255, 0.3));
86
+ }
87
+
88
+ .code-content code {
89
+ font-family: inherit;
90
+ }
91
+
92
+ /* highlight.js 主题 */
93
+ .code-content .hljs-keyword {
94
+ color: #c678dd;
95
+ }
96
+ .code-content .hljs-string {
97
+ color: #98c379;
98
+ }
99
+ .code-content .hljs-number {
100
+ color: #d19a66;
101
+ }
102
+ .code-content .hljs-comment {
103
+ color: #5c6370;
104
+ font-style: italic;
105
+ }
106
+ .code-content .hljs-function {
107
+ color: #61afef;
108
+ }
109
+ .code-content .hljs-class {
110
+ color: #e5c07b;
111
+ }
112
+ .code-content .hljs-variable {
113
+ color: #e06c75;
114
+ }
115
+ .code-content .hljs-operator {
116
+ color: #56b6c2;
117
+ }
118
+ .code-content .hljs-punctuation {
119
+ color: #abb2bf;
120
+ }
121
+ .code-content .hljs-property {
122
+ color: #e06c75;
123
+ }
124
+ .code-content .hljs-attr {
125
+ color: #d19a66;
126
+ }
127
+ .code-content .hljs-built_in {
128
+ color: #e5c07b;
129
+ }
130
+ .code-content .hljs-title {
131
+ color: #61afef;
132
+ }
133
+ .code-content .hljs-params {
134
+ color: #abb2bf;
135
+ }
136
+ .code-content .hljs-literal {
137
+ color: #56b6c2;
138
+ }
139
+ .code-content .hljs-type {
140
+ color: #e5c07b;
141
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 内容块组件导出
3
+ */
4
+
5
+ export { TextBlock } from './TextBlock'
6
+ export { CodeBlock } from './CodeBlock'
@@ -0,0 +1,78 @@
1
+ .collapsible-card {
2
+ margin: 8px 0;
3
+ border-radius: 8px;
4
+ background: var(--chat-muted, #2a2a2a);
5
+ border: 1px solid var(--chat-border, #333);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .card-header {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: 8px;
14
+ padding: 8px 12px;
15
+ user-select: none;
16
+ }
17
+
18
+ .card-header-left {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 8px;
22
+ flex: 1;
23
+ cursor: pointer;
24
+ }
25
+
26
+ .card-header-right {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 4px;
30
+ flex-shrink: 0;
31
+ }
32
+
33
+ .card-icon {
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ flex-shrink: 0;
38
+ }
39
+
40
+ .spinning {
41
+ animation: spin 1s linear infinite;
42
+ }
43
+
44
+ @keyframes spin {
45
+ from {
46
+ transform: rotate(0deg);
47
+ }
48
+ to {
49
+ transform: rotate(360deg);
50
+ }
51
+ }
52
+
53
+ .card-title {
54
+ font-size: 13px;
55
+ color: var(--chat-text-muted, #888);
56
+ }
57
+
58
+ .card-subtitle {
59
+ font-size: 12px;
60
+ color: var(--chat-text-muted, #666);
61
+ }
62
+
63
+ .card-chevron {
64
+ color: var(--chat-text-muted, #666);
65
+ transition: transform 0.2s;
66
+ flex-shrink: 0;
67
+ cursor: pointer;
68
+ }
69
+
70
+ .collapsible-card.expanded .card-chevron {
71
+ transform: rotate(180deg);
72
+ }
73
+
74
+ .card-content {
75
+ padding: 12px;
76
+ min-width: 0;
77
+ overflow: hidden;
78
+ }
@@ -0,0 +1,77 @@
1
+ import type { FC, ReactNode } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './CollapsibleCard.css'
4
+
5
+ interface CollapsibleCardProps {
6
+ /** 图标名称 */
7
+ icon: string
8
+ /** 图标颜色 */
9
+ iconColor?: string
10
+ /** 标题 */
11
+ title: string
12
+ /** 标题颜色 */
13
+ titleColor?: string
14
+ /** 副标题 */
15
+ subtitle?: string
16
+ /** 是否展开 */
17
+ expanded: boolean
18
+ /** 展开状态变化回调 */
19
+ onExpandedChange: (expanded: boolean) => void
20
+ /** 图标是否旋转(加载状态) */
21
+ spinning?: boolean
22
+ /** 是否可折叠(显示 chevron) */
23
+ collapsible?: boolean
24
+ /** 子内容 */
25
+ children?: ReactNode
26
+ /** Header 操作按钮 */
27
+ headerActions?: ReactNode
28
+ }
29
+
30
+ export const CollapsibleCard: FC<CollapsibleCardProps> = ({
31
+ icon,
32
+ iconColor = 'var(--chat-accent, #3b82f6)',
33
+ title,
34
+ titleColor,
35
+ subtitle,
36
+ expanded,
37
+ onExpandedChange,
38
+ spinning = false,
39
+ collapsible = true,
40
+ children,
41
+ headerActions,
42
+ }) => {
43
+ return (
44
+ <div className={`collapsible-card ${expanded ? 'expanded' : ''}`}>
45
+ <div className="card-header">
46
+ <div className="card-header-left" onClick={() => onExpandedChange(!expanded)}>
47
+ <div className="card-icon" style={{ color: iconColor }}>
48
+ {spinning ? (
49
+ <Icon icon="lucide:loader-2" width={14} className="spinning" />
50
+ ) : (
51
+ <Icon icon={icon} width={14} />
52
+ )}
53
+ </div>
54
+ <span className="card-title" style={titleColor ? { color: titleColor } : undefined}>
55
+ {title}
56
+ </span>
57
+ {subtitle && <span className="card-subtitle">{subtitle}</span>}
58
+ </div>
59
+ <div className="card-header-right">
60
+ {headerActions}
61
+ {collapsible && (
62
+ <Icon
63
+ icon={expanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
64
+ width={14}
65
+ className="card-chevron"
66
+ onClick={(e) => {
67
+ e.stopPropagation()
68
+ onExpandedChange(!expanded)
69
+ }}
70
+ />
71
+ )}
72
+ </div>
73
+ </div>
74
+ {expanded && <div className="card-content">{children}</div>}
75
+ </div>
76
+ )
77
+ }
@@ -0,0 +1,9 @@
1
+ .error-content {
2
+ font-size: 13px;
3
+ line-height: 1.5;
4
+ color: var(--chat-text-muted, #999);
5
+ white-space: pre-wrap;
6
+ word-break: break-word;
7
+ overflow-wrap: break-word;
8
+ margin: 0;
9
+ }
@@ -0,0 +1,40 @@
1
+ import { useState, type FC } from 'react'
2
+ import { CollapsibleCard } from './CollapsibleCard'
3
+ import './ErrorPart.css'
4
+
5
+ interface ErrorPartProps {
6
+ message: string
7
+ category?: string
8
+ retryable?: boolean
9
+ }
10
+
11
+ /** 分类标签映射 */
12
+ const categoryLabels: Record<string, string> = {
13
+ api: 'API 错误',
14
+ network: '网络错误',
15
+ timeout: '请求超时',
16
+ auth: '认证失败',
17
+ rate_limit: '请求限流',
18
+ server: '服务器错误',
19
+ unknown: '未知错误',
20
+ }
21
+
22
+ export const ErrorPart: FC<ErrorPartProps> = ({ message, category }) => {
23
+ // 默认展开显示错误信息
24
+ const [expanded, setExpanded] = useState(true)
25
+
26
+ const categoryLabel = categoryLabels[category || 'unknown'] || category || '请求失败'
27
+
28
+ return (
29
+ <CollapsibleCard
30
+ icon="lucide:alert-circle"
31
+ iconColor="var(--chat-error, #ef4444)"
32
+ title={categoryLabel}
33
+ titleColor="var(--chat-error, #ef4444)"
34
+ expanded={expanded}
35
+ onExpandedChange={setExpanded}
36
+ >
37
+ <div className="error-content">{message}</div>
38
+ </CollapsibleCard>
39
+ )
40
+ }
@@ -0,0 +1,50 @@
1
+ .image-part {
2
+ position: relative;
3
+ margin: 8px 0;
4
+ max-width: 100%;
5
+ display: inline-block;
6
+ }
7
+
8
+ .image-content {
9
+ max-width: 100%;
10
+ max-height: 400px;
11
+ border-radius: 8px;
12
+ cursor: pointer;
13
+ transition: opacity 0.2s;
14
+ }
15
+
16
+ .image-content:hover {
17
+ opacity: 0.9;
18
+ }
19
+
20
+ .image-loading,
21
+ .image-error {
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ justify-content: center;
26
+ gap: 8px;
27
+ padding: 24px;
28
+ background: var(--chat-muted, #2a2a2a);
29
+ border-radius: 8px;
30
+ color: var(--chat-text-muted, #888);
31
+ min-width: 200px;
32
+ min-height: 150px;
33
+ }
34
+
35
+ .spinning {
36
+ animation: spin 1s linear infinite;
37
+ }
38
+
39
+ @keyframes spin {
40
+ from {
41
+ transform: rotate(0deg);
42
+ }
43
+ to {
44
+ transform: rotate(360deg);
45
+ }
46
+ }
47
+
48
+ .image-error span {
49
+ font-size: 13px;
50
+ }
@@ -0,0 +1,54 @@
1
+ import { useState, type FC } from 'react'
2
+ import { Icon } from '@iconify/react'
3
+ import './ImagePart.css'
4
+
5
+ interface ImagePartProps {
6
+ url: string
7
+ alt?: string
8
+ }
9
+
10
+ export const ImagePart: FC<ImagePartProps> = ({ url, alt }) => {
11
+ const [loading, setLoading] = useState(true)
12
+ const [error, setError] = useState(false)
13
+
14
+ const handleLoad = () => {
15
+ setLoading(false)
16
+ setError(false)
17
+ }
18
+
19
+ const handleError = () => {
20
+ setLoading(false)
21
+ setError(true)
22
+ }
23
+
24
+ const openPreview = () => {
25
+ window.open(url, '_blank')
26
+ }
27
+
28
+ return (
29
+ <div className="image-part">
30
+ {!error && (
31
+ <img
32
+ src={url}
33
+ alt={alt || '图片'}
34
+ className="image-content"
35
+ onClick={openPreview}
36
+ onLoad={handleLoad}
37
+ onError={handleError}
38
+ style={{ display: loading ? 'none' : 'block' }}
39
+ />
40
+ )}
41
+ {loading && !error && (
42
+ <div className="image-loading">
43
+ <Icon icon="lucide:loader-2" width={24} className="spinning" />
44
+ </div>
45
+ )}
46
+ {error && (
47
+ <div className="image-error">
48
+ <Icon icon="lucide:image-off" width={24} />
49
+ <span>图片加载失败</span>
50
+ </div>
51
+ )}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,44 @@
1
+ .search-results {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 8px;
5
+ }
6
+
7
+ .search-item {
8
+ display: block;
9
+ padding: 8px;
10
+ border-radius: 6px;
11
+ background: var(--chat-bg, #1e1e1e);
12
+ text-decoration: none;
13
+ transition: background 0.15s;
14
+ }
15
+
16
+ .search-item:hover {
17
+ background: var(--chat-hover, #333);
18
+ }
19
+
20
+ .search-item-title {
21
+ font-size: 13px;
22
+ font-weight: 500;
23
+ color: var(--chat-accent, #3b82f6);
24
+ margin-bottom: 4px;
25
+ }
26
+
27
+ .search-item-snippet {
28
+ font-size: 12px;
29
+ color: var(--chat-text-muted, #888);
30
+ line-height: 1.4;
31
+ margin-bottom: 4px;
32
+ display: -webkit-box;
33
+ -webkit-line-clamp: 2;
34
+ -webkit-box-orient: vertical;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .search-item-url {
39
+ font-size: 11px;
40
+ color: var(--chat-text-muted, #666);
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ }
@@ -0,0 +1,63 @@
1
+ import { useState, useEffect, type FC } from 'react'
2
+ import { CollapsibleCard } from './CollapsibleCard'
3
+ import type { SearchResult, StepsExpandedType } from '../../../types'
4
+ import './SearchPart.css'
5
+
6
+ interface SearchPartProps {
7
+ query?: string
8
+ results?: SearchResult[]
9
+ status: 'running' | 'done'
10
+ expandedType?: StepsExpandedType
11
+ }
12
+
13
+ export const SearchPart: FC<SearchPartProps> = ({
14
+ results,
15
+ status,
16
+ expandedType = 'auto'
17
+ }) => {
18
+ // 根据模式计算初始状态
19
+ const getInitialExpanded = () => {
20
+ if (expandedType === 'open') return true
21
+ if (expandedType === 'close') return false
22
+ // auto: 运行时展开
23
+ return status === 'running'
24
+ }
25
+
26
+ const [expanded, setExpanded] = useState(getInitialExpanded)
27
+
28
+ // auto 模式下:状态变化时自动折叠/展开
29
+ useEffect(() => {
30
+ if (expandedType === 'auto') {
31
+ setExpanded(status === 'running')
32
+ }
33
+ }, [status, expandedType])
34
+
35
+ return (
36
+ <CollapsibleCard
37
+ icon="lucide:search"
38
+ iconColor="var(--chat-accent, #3b82f6)"
39
+ title={status === 'running' ? '搜索中...' : `搜索完成 (${results?.length || 0} 条结果)`}
40
+ spinning={status === 'running'}
41
+ expanded={expanded}
42
+ onExpandedChange={setExpanded}
43
+ >
44
+ {results && results.length > 0 && (
45
+ <div className="search-results">
46
+ {results.map((item, i) => (
47
+ <a
48
+ key={i}
49
+ href={item.url}
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ className="search-item"
53
+ >
54
+ <div className="search-item-title">{item.title}</div>
55
+ <div className="search-item-snippet">{item.snippet}</div>
56
+ <div className="search-item-url">{item.url}</div>
57
+ </a>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </CollapsibleCard>
62
+ )
63
+ }
@@ -0,0 +1,10 @@
1
+ /* 导入 Markdown 渲染样式 */
2
+ @import '@huyooo/ai-chat-shared/styles';
3
+
4
+ .text-part {
5
+ font-size: 14px;
6
+ line-height: 1.7;
7
+ color: var(--chat-text, #ccc);
8
+ word-break: break-word;
9
+ overflow-wrap: break-word;
10
+ }
@@ -0,0 +1,20 @@
1
+ import type { FC } from 'react'
2
+ import { renderMarkdown } from '@huyooo/ai-chat-shared'
3
+ import './TextPart.css'
4
+
5
+ interface TextPartProps {
6
+ text: string
7
+ }
8
+
9
+ export const TextPart: FC<TextPartProps> = ({ text }) => {
10
+ if (!text) return null
11
+
12
+ const html = renderMarkdown(text)
13
+
14
+ return (
15
+ <div
16
+ className="text-part markdown-content"
17
+ dangerouslySetInnerHTML={{ __html: html }}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,9 @@
1
+ .thinking-content {
2
+ font-size: 13px;
3
+ line-height: 1.5;
4
+ color: var(--chat-text-muted, #999);
5
+ white-space: pre-wrap;
6
+ word-break: break-word;
7
+ overflow-wrap: break-word;
8
+ margin: 0;
9
+ }