@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,11 @@
1
+ /**
2
+ * Part 渲染组件导出
3
+ */
4
+ export { CollapsibleCard } from './CollapsibleCard'
5
+ export { TextPart } from './TextPart'
6
+ export { ThinkingPart } from './ThinkingPart'
7
+ export { SearchPart } from './SearchPart'
8
+ export { ToolCallPart } from './ToolCallPart'
9
+ export { ToolResultPart } from './ToolResultPart'
10
+ export { ImagePart } from './ImagePart'
11
+ export { ErrorPart } from './ErrorPart'
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 默认工具结果渲染器
3
+ */
4
+
5
+ import { FC, useMemo } from 'react'
6
+ import type { ToolRendererProps } from '@huyooo/ai-chat-shared'
7
+ import './tool-results.css'
8
+
9
+ export const DefaultToolResult: FC<ToolRendererProps> = ({ toolResult }) => {
10
+ const formattedResult = useMemo(() => {
11
+ if (typeof toolResult === 'string') {
12
+ return toolResult
13
+ }
14
+ try {
15
+ return JSON.stringify(toolResult, null, 2)
16
+ } catch {
17
+ return String(toolResult)
18
+ }
19
+ }, [toolResult])
20
+
21
+ return (
22
+ <div className="default-tool-result">
23
+ <pre className="result-content">{formattedResult}</pre>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 搜索结果渲染器
3
+ */
4
+
5
+ import { FC, useMemo, useCallback } from 'react'
6
+ import { Icon } from '@iconify/react'
7
+ import type { ToolRendererProps, SearchResultItem } from '@huyooo/ai-chat-shared'
8
+ import './tool-results.css'
9
+
10
+ export const SearchResults: FC<ToolRendererProps> = ({ toolResult }) => {
11
+ const results = useMemo<SearchResultItem[]>(() => {
12
+ if (Array.isArray(toolResult)) {
13
+ return toolResult as SearchResultItem[]
14
+ }
15
+ if (typeof toolResult === 'object' && toolResult !== null && 'results' in toolResult) {
16
+ return (toolResult as { results: SearchResultItem[] }).results
17
+ }
18
+ return []
19
+ }, [toolResult])
20
+
21
+ /** 获取域名 */
22
+ const getDomain = useCallback((url: string): string => {
23
+ try {
24
+ return new URL(url).hostname
25
+ } catch {
26
+ return url
27
+ }
28
+ }, [])
29
+
30
+ /** 打开外部链接 */
31
+ const openExternal = useCallback((url: string) => {
32
+ const bridge = (window as { aiChatBridge?: { openExternal: (url: string) => Promise<void> } })
33
+ .aiChatBridge
34
+ if (bridge?.openExternal) {
35
+ bridge.openExternal(url)
36
+ } else {
37
+ window.open(url, '_blank')
38
+ }
39
+ }, [])
40
+
41
+ return (
42
+ <div className="search-results-card">
43
+ <div className="search-header">
44
+ <Icon icon="lucide:search" width={16} />
45
+ <span>搜索结果</span>
46
+ <span className="search-count">{results.length} 条</span>
47
+ </div>
48
+ <div className="search-list">
49
+ {results.map((item, index) => (
50
+ <a
51
+ key={index}
52
+ href={item.url}
53
+ target="_blank"
54
+ rel="noopener noreferrer"
55
+ className="search-item"
56
+ onClick={(e) => {
57
+ e.preventDefault()
58
+ openExternal(item.url)
59
+ }}
60
+ >
61
+ <div className="item-title">{item.title}</div>
62
+ {item.snippet && <div className="item-snippet">{item.snippet}</div>}
63
+ <div className="item-url">{getDomain(item.url)}</div>
64
+ </a>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 天气卡片渲染器
3
+ */
4
+
5
+ import { FC, useMemo } from 'react'
6
+ import { Icon } from '@iconify/react'
7
+ import type { ToolRendererProps, WeatherData } from '@huyooo/ai-chat-shared'
8
+ import './tool-results.css'
9
+
10
+ export const WeatherCard: FC<ToolRendererProps> = ({ toolResult }) => {
11
+ const weather = useMemo<WeatherData>(() => {
12
+ if (typeof toolResult === 'object' && toolResult !== null) {
13
+ return toolResult as WeatherData
14
+ }
15
+ return {
16
+ city: '未知',
17
+ temperature: 0,
18
+ condition: '未知',
19
+ }
20
+ }, [toolResult])
21
+
22
+ return (
23
+ <div className="weather-card">
24
+ <div className="weather-header">
25
+ <Icon icon="lucide:cloud-sun" width={24} className="weather-icon" />
26
+ <div className="weather-location">{weather.city}</div>
27
+ </div>
28
+ <div className="weather-main">
29
+ <div className="weather-temp">{weather.temperature}°</div>
30
+ <div className="weather-condition">{weather.condition}</div>
31
+ </div>
32
+ {(weather.humidity || weather.wind) && (
33
+ <div className="weather-details">
34
+ {weather.humidity && (
35
+ <div className="weather-detail">
36
+ <Icon icon="lucide:droplets" width={14} />
37
+ <span>{weather.humidity}%</span>
38
+ </div>
39
+ )}
40
+ {weather.wind && (
41
+ <div className="weather-detail">
42
+ <Icon icon="lucide:wind" width={14} />
43
+ <span>{weather.wind}</span>
44
+ </div>
45
+ )}
46
+ </div>
47
+ )}
48
+ {weather.forecast?.length ? (
49
+ <div className="weather-forecast">
50
+ {weather.forecast.map((item) => (
51
+ <div key={item.date} className="forecast-item">
52
+ <div className="forecast-date">{item.date}</div>
53
+ <div className="forecast-temp">
54
+ {item.low}° / {item.high}°
55
+ </div>
56
+ <div className="forecast-condition">{item.condition}</div>
57
+ </div>
58
+ ))}
59
+ </div>
60
+ ) : null}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 工具结果渲染组件导出
3
+ */
4
+
5
+ export { DefaultToolResult } from './DefaultToolResult'
6
+ export { WeatherCard } from './WeatherCard'
7
+ export { SearchResults } from './SearchResults'
@@ -0,0 +1,179 @@
1
+ /**
2
+ * 工具结果渲染器共享样式
3
+ */
4
+
5
+ /* 默认工具结果 */
6
+ .default-tool-result {
7
+ background: var(--chat-bg, #1e1e1e);
8
+ border-radius: 6px;
9
+ overflow: hidden;
10
+ }
11
+
12
+ .result-content {
13
+ margin: 0;
14
+ padding: 8px;
15
+ font-size: 13px;
16
+ line-height: 1.5;
17
+ color: var(--chat-text-muted, #999);
18
+ font-family: "SF Mono", Monaco, monospace;
19
+ white-space: pre-wrap;
20
+ word-break: break-word;
21
+ overflow-x: auto;
22
+ }
23
+
24
+ /* 天气卡片 */
25
+ .weather-card {
26
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
27
+ border-radius: 12px;
28
+ padding: 16px;
29
+ color: #fff;
30
+ min-width: 200px;
31
+ }
32
+
33
+ .weather-header {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ margin-bottom: 12px;
38
+ }
39
+
40
+ .weather-icon {
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .weather-location {
45
+ font-size: 14px;
46
+ font-weight: 500;
47
+ }
48
+
49
+ .weather-main {
50
+ margin-bottom: 12px;
51
+ }
52
+
53
+ .weather-temp {
54
+ font-size: 48px;
55
+ font-weight: 300;
56
+ line-height: 1;
57
+ }
58
+
59
+ .weather-condition {
60
+ font-size: 14px;
61
+ opacity: 0.9;
62
+ margin-top: 4px;
63
+ }
64
+
65
+ .weather-details {
66
+ display: flex;
67
+ gap: 16px;
68
+ padding-top: 12px;
69
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
70
+ }
71
+
72
+ .weather-detail {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 4px;
76
+ font-size: 13px;
77
+ opacity: 0.9;
78
+ }
79
+
80
+ .weather-forecast {
81
+ margin-top: 12px;
82
+ padding-top: 12px;
83
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
84
+ display: flex;
85
+ gap: 12px;
86
+ overflow-x: auto;
87
+ }
88
+
89
+ .forecast-item {
90
+ text-align: center;
91
+ min-width: 60px;
92
+ }
93
+
94
+ .forecast-date {
95
+ font-size: 12px;
96
+ opacity: 0.8;
97
+ margin-bottom: 4px;
98
+ }
99
+
100
+ .forecast-temp {
101
+ font-size: 13px;
102
+ font-weight: 500;
103
+ }
104
+
105
+ .forecast-condition {
106
+ font-size: 11px;
107
+ opacity: 0.8;
108
+ margin-top: 2px;
109
+ }
110
+
111
+ /* 搜索结果卡片 */
112
+ .search-results-card {
113
+ background: var(--chat-muted, #2d2d2d);
114
+ border-radius: 8px;
115
+ overflow: hidden;
116
+ }
117
+
118
+ .search-header {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 8px;
122
+ padding: 10px 12px;
123
+ background: rgba(0, 0, 0, 0.2);
124
+ color: var(--chat-text-muted, #888);
125
+ font-size: 13px;
126
+ }
127
+
128
+ .search-count {
129
+ margin-left: auto;
130
+ font-size: 12px;
131
+ opacity: 0.7;
132
+ }
133
+
134
+ .search-list {
135
+ display: flex;
136
+ flex-direction: column;
137
+ }
138
+
139
+ .search-item {
140
+ display: block;
141
+ padding: 10px 12px;
142
+ text-decoration: none;
143
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
144
+ transition: background 0.15s;
145
+ }
146
+
147
+ .search-item:last-child {
148
+ border-bottom: none;
149
+ }
150
+
151
+ .search-item:hover {
152
+ background: rgba(255, 255, 255, 0.05);
153
+ }
154
+
155
+ .item-title {
156
+ font-size: 14px;
157
+ color: var(--chat-text, #ccc);
158
+ font-weight: 500;
159
+ margin-bottom: 4px;
160
+ overflow: hidden;
161
+ text-overflow: ellipsis;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ .item-snippet {
166
+ font-size: 12px;
167
+ color: var(--chat-text-muted, #888);
168
+ line-height: 1.4;
169
+ margin-bottom: 4px;
170
+ display: -webkit-box;
171
+ -webkit-line-clamp: 2;
172
+ -webkit-box-orient: vertical;
173
+ overflow: hidden;
174
+ }
175
+
176
+ .item-url {
177
+ font-size: 11px;
178
+ color: var(--chat-text-muted, #666);
179
+ }
@@ -0,0 +1,46 @@
1
+ /** 功能项 */
2
+ export interface WelcomeFeature {
3
+ name: string;
4
+ icon: string;
5
+ }
6
+
7
+ /** 快捷任务 */
8
+ export interface WelcomeTask {
9
+ name: string;
10
+ desc: string;
11
+ prompt: string;
12
+ icon: string;
13
+ }
14
+
15
+ /** 欢迎页配置 */
16
+ export interface WelcomeConfig {
17
+ /** 标题 */
18
+ title: string;
19
+ /** 副标题 */
20
+ subtitle: string;
21
+ /** 图标 */
22
+ icon: string;
23
+ /** 功能列表 */
24
+ features: WelcomeFeature[];
25
+ /** 快捷任务 */
26
+ tasks: WelcomeTask[];
27
+ }
28
+
29
+ /** 默认配置 */
30
+ export const defaultWelcomeConfig: WelcomeConfig = {
31
+ title: 'AI 助手',
32
+ subtitle: '智能对话 · 文档分析 · 内容创作',
33
+ icon: 'lucide:sparkles',
34
+ features: [
35
+ { name: '上下文引用', icon: 'lucide:at-sign' },
36
+ { name: '图片分析', icon: 'lucide:image' },
37
+ { name: '深度思考', icon: 'lucide:sparkles' },
38
+ { name: '联网搜索', icon: 'lucide:globe' },
39
+ ],
40
+ tasks: [
41
+ { name: '总结内容', desc: '快速总结文档要点', prompt: '帮我总结一下这段内容的主要要点', icon: 'lucide:file-text' },
42
+ { name: '翻译文本', desc: '多语言翻译助手', prompt: '帮我翻译这段文字', icon: 'lucide:languages' },
43
+ { name: '写作助手', desc: '帮助撰写文章', prompt: '帮我写一篇关于以下主题的文章:', icon: 'lucide:pen-tool' },
44
+ { name: '问答对话', desc: '回答各种问题', prompt: '我想了解一下:', icon: 'lucide:message-circle' },
45
+ ],
46
+ };
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { AutoRunConfig } from '@huyooo/ai-chat-bridge-electron/renderer'
3
+
4
+ export interface AutoRunConfigContextValue {
5
+ autoRunConfig: AutoRunConfig
6
+ saveAutoRunConfig: (config: AutoRunConfig) => Promise<void>
7
+ }
8
+
9
+ export const AutoRunConfigContext = createContext<AutoRunConfigContextValue | null>(null)
10
+
11
+ export function useAutoRunConfig(): AutoRunConfigContextValue | null {
12
+ return useContext(AutoRunConfigContext)
13
+ }
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from 'react'
2
+ import type { ChatAdapter } from '../adapter'
3
+
4
+ export const ChatAdapterContext = createContext<ChatAdapter | null>(null)
5
+
6
+ export function useChatAdapter(): ChatAdapter | null {
7
+ return useContext(ChatAdapterContext)
8
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 全局 ChatInput 状态 Context
3
+ * 用于在 MessageBubble 中的 ChatInput 共享和修改全局状态
4
+ */
5
+
6
+ import { createContext, useContext, type ReactNode, type FC } from 'react'
7
+ import type { ChatMode, ModelOption } from '../types'
8
+ import type { ChatAdapter } from '@huyooo/ai-chat-bridge-electron/renderer'
9
+
10
+ export interface ChatInputContextValue {
11
+ mode: ChatMode
12
+ model: string
13
+ models: ModelOption[]
14
+ webSearch: boolean
15
+ thinking: boolean
16
+ isLoading: boolean
17
+ /** Electron adapter(用于 @ 文件选择) */
18
+ adapter?: ChatAdapter
19
+ // cwd 已解耦,不再通过 context 传递
20
+ // FileBrowser 会直接通过 adapter.setCwd() 同步到 Agent
21
+ setMode: (value: ChatMode) => void
22
+ setModel: (value: string) => void
23
+ setWebSearch: (value: boolean) => void
24
+ setThinking: (value: boolean) => void
25
+ }
26
+
27
+ const ChatInputContext = createContext<ChatInputContextValue | null>(null)
28
+
29
+ interface ChatInputProviderProps {
30
+ value: ChatInputContextValue
31
+ children: ReactNode
32
+ }
33
+
34
+ export const ChatInputProvider: FC<ChatInputProviderProps> = ({ value, children }) => {
35
+ return <ChatInputContext.Provider value={value}>{children}</ChatInputContext.Provider>
36
+ }
37
+
38
+ export function useChatInputContext(): ChatInputContextValue | null {
39
+ return useContext(ChatInputContext)
40
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 渲染器上下文
3
+ * 用于注入自定义块渲染器和工具结果渲染器
4
+ */
5
+
6
+ import { createContext, type ComponentType, type ReactNode, type FC } from 'react'
7
+ import type { ContentBlock, ToolRendererProps } from '@huyooo/ai-chat-shared'
8
+
9
+ /** 块渲染器映射 */
10
+ export type BlockRenderers = Record<string, ComponentType<{ block: ContentBlock }>>
11
+
12
+ /** 工具结果渲染器映射 */
13
+ export type ToolRenderers = Record<string, ComponentType<ToolRendererProps>>
14
+
15
+ /** 块渲染器上下文 */
16
+ export const BlockRenderersContext = createContext<BlockRenderers>({})
17
+
18
+ /** 工具结果渲染器上下文 */
19
+ export const ToolRenderersContext = createContext<ToolRenderers>({})
20
+
21
+ /** 渲染器 Provider Props */
22
+ interface RenderersProviderProps {
23
+ blockRenderers?: BlockRenderers
24
+ toolRenderers?: ToolRenderers
25
+ children: ReactNode
26
+ }
27
+
28
+ /** 渲染器 Provider */
29
+ export const RenderersProvider: FC<RenderersProviderProps> = ({
30
+ blockRenderers = {},
31
+ toolRenderers = {},
32
+ children,
33
+ }) => {
34
+ return (
35
+ <BlockRenderersContext.Provider value={blockRenderers}>
36
+ <ToolRenderersContext.Provider value={toolRenderers}>
37
+ {children}
38
+ </ToolRenderersContext.Provider>
39
+ </BlockRenderersContext.Provider>
40
+ )
41
+ }