@libreapps/react 1.1.1
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.
- package/LICENSE.md +21 -0
- package/README.md +347 -0
- package/dist/index.d.mts +255 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +548 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +539 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +100 -0
- package/src/components/LibreAppsProvider.test.tsx +346 -0
- package/src/components/LibreAppsProvider.tsx +508 -0
- package/src/hooks/index.ts +39 -0
- package/src/hooks/types.ts +162 -0
- package/src/hooks/useAttachments.ts +0 -0
- package/src/hooks/useAuth.ts +0 -0
- package/src/hooks/useComponent.ts +0 -0
- package/src/hooks/useGenerativeUI.ts +0 -0
- package/src/hooks/useMCP.ts +0 -0
- package/src/hooks/useMessage.ts +105 -0
- package/src/hooks/useModelConfig.ts +0 -0
- package/src/hooks/useStreaming.ts +161 -0
- package/src/hooks/useSuggestions.ts +0 -0
- package/src/hooks/useThread.ts +0 -0
- package/src/hooks/useTool.ts +0 -0
- package/src/index.ts +40 -0
- package/src/types/index.ts +25 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/id.ts +6 -0
- package/src/utils/stream.ts +33 -0
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@libreapps/react",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "React package for building AI-powered applications with generative UI",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public",
|
|
8
|
+
"scope": "@libreapps"
|
|
9
|
+
},
|
|
10
|
+
"author": "LibreApps Contributors",
|
|
11
|
+
"license": "BSD-3-Clause",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.mjs",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/libre-apps/UI.git",
|
|
25
|
+
"directory": "pkg/react"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"ai",
|
|
29
|
+
"react",
|
|
30
|
+
"generative-ui",
|
|
31
|
+
"llm",
|
|
32
|
+
"chat",
|
|
33
|
+
"copilot",
|
|
34
|
+
"libreapps",
|
|
35
|
+
"mcp",
|
|
36
|
+
"model-context-protocol"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.mjs",
|
|
42
|
+
"require": "./dist/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./hooks": {
|
|
45
|
+
"types": "./dist/hooks/index.d.ts",
|
|
46
|
+
"import": "./dist/hooks/index.mjs",
|
|
47
|
+
"require": "./dist/hooks/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./components": {
|
|
50
|
+
"types": "./dist/components/index.d.ts",
|
|
51
|
+
"import": "./dist/components/index.mjs",
|
|
52
|
+
"require": "./dist/components/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./tools": {
|
|
55
|
+
"types": "./dist/tools/index.d.ts",
|
|
56
|
+
"import": "./dist/tools/index.mjs",
|
|
57
|
+
"require": "./dist/tools/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./mcp": {
|
|
60
|
+
"types": "./dist/mcp/index.d.ts",
|
|
61
|
+
"import": "./dist/mcp/index.mjs",
|
|
62
|
+
"require": "./dist/mcp/index.js"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
67
|
+
"clsx": "^2.1.1",
|
|
68
|
+
"nanoid": "^5.0.0",
|
|
69
|
+
"openai": "^4.0.0",
|
|
70
|
+
"swr": "^2.3.0",
|
|
71
|
+
"tailwind-merge": "^3.3.1",
|
|
72
|
+
"zod": "^3.25.0",
|
|
73
|
+
"zustand": "^5.0.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
77
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@testing-library/react": "^16.3.0",
|
|
81
|
+
"@types/node": "^20.14.15",
|
|
82
|
+
"@types/react": "^18.3.1",
|
|
83
|
+
"@types/react-dom": "^18.3.1",
|
|
84
|
+
"@vitest/coverage-v8": "^4.0.6",
|
|
85
|
+
"@vitest/ui": "^4.0.6",
|
|
86
|
+
"react": "^18.3.1",
|
|
87
|
+
"react-dom": "^18.3.1",
|
|
88
|
+
"tsup": "^8.5.0",
|
|
89
|
+
"typescript": "^5.6.3",
|
|
90
|
+
"vitest": "^4.0.6"
|
|
91
|
+
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "tsup",
|
|
94
|
+
"dev": "tsup --watch",
|
|
95
|
+
"test": "vitest",
|
|
96
|
+
"test:coverage": "vitest --coverage",
|
|
97
|
+
"typecheck": "tsc --noEmit",
|
|
98
|
+
"clean": "rm -rf dist"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { LibreAppsProvider, useLibreApps } from './LibreAppsProvider'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
// Mock fetch
|
|
9
|
+
global.fetch = vi.fn()
|
|
10
|
+
|
|
11
|
+
describe('LibreAppsProvider', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should provide context to children', () => {
|
|
17
|
+
const TestComponent = () => {
|
|
18
|
+
const context = useLibreApps()
|
|
19
|
+
return <div>Context: {context ? 'Available' : 'Not Available'}</div>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
render(
|
|
23
|
+
<LibreAppsProvider apiKey="test-key">
|
|
24
|
+
<TestComponent />
|
|
25
|
+
</LibreAppsProvider>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText('Context: Available')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should throw error when useLibreApps is used outside provider', () => {
|
|
32
|
+
const TestComponent = () => {
|
|
33
|
+
const context = useLibreApps()
|
|
34
|
+
return <div>{context}</div>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Suppress console.error for this test
|
|
38
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
39
|
+
|
|
40
|
+
expect(() => render(<TestComponent />)).toThrow(
|
|
41
|
+
'useLibreApps must be used within LibreAppsProvider'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
spy.mockRestore()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should initialize with default thread', async () => {
|
|
48
|
+
const TestComponent = () => {
|
|
49
|
+
const { threads, activeThreadId } = useLibreApps()
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<div>Threads: {threads.size}</div>
|
|
53
|
+
<div>Active: {activeThreadId || 'none'}</div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render(
|
|
59
|
+
<LibreAppsProvider apiKey="test-key">
|
|
60
|
+
<TestComponent />
|
|
61
|
+
</LibreAppsProvider>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(screen.getByText(/Threads: 1/)).toBeInTheDocument()
|
|
66
|
+
expect(screen.getByText(/Active: /)).toBeInTheDocument()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should register components', () => {
|
|
71
|
+
const TestComponent = () => <div>Test Component</div>
|
|
72
|
+
const components = [
|
|
73
|
+
{
|
|
74
|
+
name: 'test',
|
|
75
|
+
component: TestComponent,
|
|
76
|
+
description: 'Test component'
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const ConsumerComponent = () => {
|
|
81
|
+
const { components: registeredComponents } = useLibreApps()
|
|
82
|
+
return <div>Components: {registeredComponents.size}</div>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(
|
|
86
|
+
<LibreAppsProvider apiKey="test-key" components={components}>
|
|
87
|
+
<ConsumerComponent />
|
|
88
|
+
</LibreAppsProvider>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(screen.getByText('Components: 1')).toBeInTheDocument()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should register tools', () => {
|
|
95
|
+
const tools = [
|
|
96
|
+
{
|
|
97
|
+
name: 'calculator',
|
|
98
|
+
description: 'Calculate math',
|
|
99
|
+
parameters: z.object({ expression: z.string() }),
|
|
100
|
+
execute: async ({ expression }: any) => ({ result: eval(expression) })
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const ConsumerComponent = () => {
|
|
105
|
+
const { tools: registeredTools } = useLibreApps()
|
|
106
|
+
return <div>Tools: {registeredTools.size}</div>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<LibreAppsProvider apiKey="test-key" tools={tools}>
|
|
111
|
+
<ConsumerComponent />
|
|
112
|
+
</LibreAppsProvider>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(screen.getByText('Tools: 1')).toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should send message to API', async () => {
|
|
119
|
+
const mockResponse = {
|
|
120
|
+
choices: [{
|
|
121
|
+
message: {
|
|
122
|
+
content: 'AI response'
|
|
123
|
+
}
|
|
124
|
+
}]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
;(global.fetch as any).mockResolvedValueOnce({
|
|
128
|
+
ok: true,
|
|
129
|
+
json: async () => mockResponse
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const TestComponent = () => {
|
|
133
|
+
const { sendMessage } = useLibreApps()
|
|
134
|
+
const [response, setResponse] = React.useState<any>(null)
|
|
135
|
+
|
|
136
|
+
const handleSend = async () => {
|
|
137
|
+
const msg = await sendMessage('Hello')
|
|
138
|
+
setResponse(msg)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div>
|
|
143
|
+
<button onClick={handleSend}>Send</button>
|
|
144
|
+
{response && <div>Response: {response.content}</div>}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
render(
|
|
150
|
+
<LibreAppsProvider apiKey="test-key">
|
|
151
|
+
<TestComponent />
|
|
152
|
+
</LibreAppsProvider>
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const button = screen.getByText('Send')
|
|
156
|
+
await userEvent.click(button)
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(screen.getByText('Response: AI response')).toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
163
|
+
'https://api.libreapps.com/v1/chat/completions',
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: expect.objectContaining({
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'Authorization': 'Bearer test-key'
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should handle API errors', async () => {
|
|
175
|
+
;(global.fetch as any).mockRejectedValueOnce(new Error('Network error'))
|
|
176
|
+
|
|
177
|
+
const onError = vi.fn()
|
|
178
|
+
|
|
179
|
+
const TestComponent = () => {
|
|
180
|
+
const { sendMessage } = useLibreApps()
|
|
181
|
+
const [error, setError] = React.useState<any>(null)
|
|
182
|
+
|
|
183
|
+
const handleSend = async () => {
|
|
184
|
+
try {
|
|
185
|
+
await sendMessage('Hello')
|
|
186
|
+
} catch (err) {
|
|
187
|
+
setError(err)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
<button onClick={handleSend}>Send</button>
|
|
194
|
+
{error && <div>Error: {error.message}</div>}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<LibreAppsProvider apiKey="test-key" onError={onError}>
|
|
201
|
+
<TestComponent />
|
|
202
|
+
</LibreAppsProvider>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const button = screen.getByText('Send')
|
|
206
|
+
await userEvent.click(button)
|
|
207
|
+
|
|
208
|
+
await waitFor(() => {
|
|
209
|
+
expect(screen.getByText('Error: Network error')).toBeInTheDocument()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error))
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should execute tools', async () => {
|
|
216
|
+
const mockExecute = vi.fn().mockResolvedValue({ result: 42 })
|
|
217
|
+
|
|
218
|
+
const tools = [
|
|
219
|
+
{
|
|
220
|
+
name: 'calculator',
|
|
221
|
+
description: 'Calculate',
|
|
222
|
+
parameters: z.object({ expression: z.string() }),
|
|
223
|
+
execute: mockExecute
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
const TestComponent = () => {
|
|
228
|
+
const { executeTool } = useLibreApps()
|
|
229
|
+
const [result, setResult] = React.useState<any>(null)
|
|
230
|
+
|
|
231
|
+
const handleExecute = async () => {
|
|
232
|
+
const res = await executeTool('calculator', { expression: '2+2' })
|
|
233
|
+
setResult(res)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div>
|
|
238
|
+
<button onClick={handleExecute}>Execute</button>
|
|
239
|
+
{result && <div>Result: {result.result}</div>}
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
render(
|
|
245
|
+
<LibreAppsProvider apiKey="test-key" tools={tools}>
|
|
246
|
+
<TestComponent />
|
|
247
|
+
</LibreAppsProvider>
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const button = screen.getByText('Execute')
|
|
251
|
+
await userEvent.click(button)
|
|
252
|
+
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(screen.getByText('Result: 42')).toBeInTheDocument()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(mockExecute).toHaveBeenCalledWith({ expression: '2+2' })
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should render components dynamically', () => {
|
|
261
|
+
const CustomComponent = ({ text }: { text: string }) => (
|
|
262
|
+
<div>Custom: {text}</div>
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const components = [
|
|
266
|
+
{
|
|
267
|
+
name: 'custom',
|
|
268
|
+
component: CustomComponent,
|
|
269
|
+
description: 'Custom component'
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
const TestComponent = () => {
|
|
274
|
+
const { renderComponent } = useLibreApps()
|
|
275
|
+
const rendered = renderComponent('custom', { text: 'Hello' })
|
|
276
|
+
|
|
277
|
+
return <div>{rendered}</div>
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
render(
|
|
281
|
+
<LibreAppsProvider apiKey="test-key" components={components}>
|
|
282
|
+
<TestComponent />
|
|
283
|
+
</LibreAppsProvider>
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
expect(screen.getByText('Custom: Hello')).toBeInTheDocument()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should manage thread operations', async () => {
|
|
290
|
+
const TestComponent = () => {
|
|
291
|
+
const {
|
|
292
|
+
threads,
|
|
293
|
+
createThread,
|
|
294
|
+
switchThread,
|
|
295
|
+
deleteThread,
|
|
296
|
+
activeThreadId
|
|
297
|
+
} = useLibreApps()
|
|
298
|
+
|
|
299
|
+
const handleCreate = () => {
|
|
300
|
+
const thread = createThread({ name: 'New Thread' })
|
|
301
|
+
switchThread(thread.id)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const handleDelete = () => {
|
|
305
|
+
if (activeThreadId) {
|
|
306
|
+
deleteThread(activeThreadId)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div>
|
|
312
|
+
<div>Thread Count: {threads.size}</div>
|
|
313
|
+
<button onClick={handleCreate}>Create Thread</button>
|
|
314
|
+
<button onClick={handleDelete}>Delete Thread</button>
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
render(
|
|
320
|
+
<LibreAppsProvider apiKey="test-key">
|
|
321
|
+
<TestComponent />
|
|
322
|
+
</LibreAppsProvider>
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
// Initially should have 1 thread
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(screen.getByText('Thread Count: 1')).toBeInTheDocument()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Create a new thread
|
|
331
|
+
const createButton = screen.getByText('Create Thread')
|
|
332
|
+
await userEvent.click(createButton)
|
|
333
|
+
|
|
334
|
+
await waitFor(() => {
|
|
335
|
+
expect(screen.getByText('Thread Count: 2')).toBeInTheDocument()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Delete current thread
|
|
339
|
+
const deleteButton = screen.getByText('Delete Thread')
|
|
340
|
+
await userEvent.click(deleteButton)
|
|
341
|
+
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(screen.getByText('Thread Count: 1')).toBeInTheDocument()
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|