@blocklet/aigne-hub 0.4.43 → 0.4.44

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.
@@ -5,15 +5,142 @@ const immer_1 = require("immer");
5
5
  const nanoid_1 = require("nanoid");
6
6
  const react_1 = require("react");
7
7
  const nextId = () => (0, nanoid_1.nanoid)(16);
8
- function useConversation({ scrollToBottom, textCompletions, imageGenerations, }) {
9
- const [messages, setMessages] = (0, react_1.useState)(() => [
10
- { id: nextId(), response: 'Hi, I am AIGNE Hub! How can I assist you today?' },
11
- ]);
8
+ const STORAGE_KEY = 'aigne-hub-conversation-history';
9
+ const SESSION_STORAGE_KEY = 'aigne-hub-conversation-session';
10
+ const MAX_CACHED_MESSAGES = 5; // Limit cached messages to reduce storage usage
11
+ // Load messages from localStorage/sessionStorage
12
+ const loadMessages = async () => {
13
+ try {
14
+ // Try localStorage first, then sessionStorage as fallback
15
+ let cached = localStorage.getItem(STORAGE_KEY);
16
+ if (!cached) {
17
+ cached = sessionStorage.getItem(SESSION_STORAGE_KEY);
18
+ }
19
+ if (cached) {
20
+ const parsed = JSON.parse(cached);
21
+ // Validate and filter out invalid messages
22
+ if (Array.isArray(parsed) && parsed.length > 0) {
23
+ const validMessages = parsed.filter((msg) => msg.id && (msg.prompt || msg.response));
24
+ return validMessages;
25
+ }
26
+ }
27
+ }
28
+ catch (error) {
29
+ console.warn('Failed to load conversation history:', error);
30
+ }
31
+ // Return empty array if no history, let the component handle initial message
32
+ return [];
33
+ };
34
+ // Save messages to localStorage
35
+ const saveMessages = async (messages) => {
36
+ try {
37
+ // Only save completed messages (no loading state)
38
+ const toSave = messages
39
+ .filter((msg) => !msg.loading)
40
+ .slice(-MAX_CACHED_MESSAGES) // Keep only last N messages
41
+ .map((msg) => {
42
+ var _a;
43
+ // Replace image data URLs with placeholders to save storage space
44
+ const response = msg.response && typeof msg.response === 'object' && 'images' in msg.response
45
+ ? {
46
+ ...msg.response,
47
+ images: ((_a = msg.response.images) === null || _a === void 0 ? void 0 : _a.map((img) => ({
48
+ ...img,
49
+ url: img.url && img.url.startsWith('data:') ? '[IMAGE_PLACEHOLDER]' : img.url,
50
+ }))) || [],
51
+ }
52
+ : msg.response;
53
+ return {
54
+ id: msg.id,
55
+ prompt: msg.prompt,
56
+ response,
57
+ timestamp: msg.timestamp,
58
+ // Don't save meta and error state to reduce size
59
+ };
60
+ });
61
+ // Try to save to localStorage, fallback to sessionStorage if quota exceeded
62
+ try {
63
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
64
+ // Clear sessionStorage if localStorage succeeded
65
+ sessionStorage.removeItem(SESSION_STORAGE_KEY);
66
+ }
67
+ catch (storageError) {
68
+ if (storageError instanceof Error && storageError.name === 'QuotaExceededError') {
69
+ // Fallback to sessionStorage
70
+ try {
71
+ sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(toSave));
72
+ }
73
+ catch (sessionError) {
74
+ console.warn('Failed to save to sessionStorage:', sessionError);
75
+ }
76
+ }
77
+ else {
78
+ console.warn('Failed to save to localStorage:', storageError);
79
+ }
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.warn('Failed to save conversation history:', error);
84
+ }
85
+ };
86
+ function useConversation({ scrollToBottom, textCompletions, imageGenerations, enableCache = true, }) {
87
+ const [messages, setMessages] = (0, react_1.useState)([]);
88
+ const [isInitialLoad, setIsInitialLoad] = (0, react_1.useState)(true);
89
+ const [isLoadingHistory, setIsLoadingHistory] = (0, react_1.useState)(enableCache);
90
+ // Load initial messages from cache
91
+ (0, react_1.useEffect)(() => {
92
+ if (enableCache && isLoadingHistory) {
93
+ loadMessages().then((loadedMessages) => {
94
+ // If no messages loaded, show welcome message
95
+ if (loadedMessages.length === 0) {
96
+ setMessages([
97
+ { id: nextId(), response: 'Hi, I am AIGNE Hub! How can I assist you today?', timestamp: Date.now() },
98
+ ]);
99
+ }
100
+ else {
101
+ setMessages(loadedMessages);
102
+ }
103
+ setIsLoadingHistory(false);
104
+ });
105
+ }
106
+ else if (!enableCache) {
107
+ setMessages([
108
+ { id: nextId(), response: 'Hi, I am AIGNE Hub! How can I assist you today?', timestamp: Date.now() },
109
+ ]);
110
+ setIsLoadingHistory(false);
111
+ }
112
+ }, [enableCache, isLoadingHistory]);
113
+ // Scroll to bottom on initial load
114
+ (0, react_1.useEffect)(() => {
115
+ if (isInitialLoad && messages.length > 0 && !isLoadingHistory) {
116
+ // Wait for DOM to render
117
+ setTimeout(() => {
118
+ scrollToBottom === null || scrollToBottom === void 0 ? void 0 : scrollToBottom({ force: true });
119
+ setIsInitialLoad(false);
120
+ }, 100);
121
+ }
122
+ }, [isInitialLoad, messages.length, scrollToBottom, isLoadingHistory]);
123
+ // Save messages to localStorage whenever they change (with debounce)
124
+ (0, react_1.useEffect)(() => {
125
+ if (enableCache && messages.length > 0) {
126
+ const timeoutId = setTimeout(() => {
127
+ saveMessages(messages).catch((error) => {
128
+ console.error('❌ Failed to save messages:', error);
129
+ });
130
+ }, 1000); // Delay save by 1 second to avoid saving during streaming
131
+ return () => clearTimeout(timeoutId);
132
+ }
133
+ return () => { };
134
+ }, [messages, enableCache]);
12
135
  const add = (0, react_1.useCallback)(async (prompt, meta) => {
13
136
  const id = nextId();
14
- setMessages((v) => v.concat({ id, prompt, loading: true, meta }));
15
- scrollToBottom === null || scrollToBottom === void 0 ? void 0 : scrollToBottom({ force: true });
137
+ const timestamp = Date.now();
138
+ setMessages((v) => v.concat({ id, prompt, loading: true, meta, timestamp }));
139
+ setTimeout(() => {
140
+ scrollToBottom === null || scrollToBottom === void 0 ? void 0 : scrollToBottom({ force: true });
141
+ }, 100);
16
142
  try {
143
+ // Handle image generation command
17
144
  if (imageGenerations && typeof prompt === 'string') {
18
145
  const m = prompt.match(/^\/image(\s+(?<size>256|512|1024))?(\s+(?<n>[1-9]|10))?\s+(?<prompt>[\s\S]+)/);
19
146
  if (m === null || m === void 0 ? void 0 : m.groups) {
@@ -49,7 +176,7 @@ function useConversation({ scrollToBottom, textCompletions, imageGenerations, })
49
176
  response = value.text;
50
177
  }
51
178
  else if (isImages(value)) {
52
- response = value.images;
179
+ response = { images: value.images };
53
180
  }
54
181
  else {
55
182
  delta = decoder.decode(value);
@@ -76,8 +203,13 @@ function useConversation({ scrollToBottom, textCompletions, imageGenerations, })
76
203
  catch (error) {
77
204
  setMessages((v) => (0, immer_1.produce)(v, (draft) => {
78
205
  const item = draft.find((i) => i.id === id);
79
- if (item)
80
- item.error = error;
206
+ if (item) {
207
+ // Format error for CreditErrorAlert component
208
+ item.error = {
209
+ message: error instanceof Error ? error.message : String(error),
210
+ type: 'unknown',
211
+ };
212
+ }
81
213
  }));
82
214
  return null;
83
215
  }
@@ -96,5 +228,24 @@ function useConversation({ scrollToBottom, textCompletions, imageGenerations, })
96
228
  i.loading = false;
97
229
  }));
98
230
  }, []);
99
- return { messages, add, cancel, setMessages };
231
+ const clearHistory = (0, react_1.useCallback)(async () => {
232
+ const initialMessage = {
233
+ id: nextId(),
234
+ response: 'Hi, I am AIGNE Hub! How can I assist you today?',
235
+ timestamp: Date.now(),
236
+ };
237
+ setMessages([initialMessage]);
238
+ if (enableCache) {
239
+ localStorage.removeItem(STORAGE_KEY);
240
+ sessionStorage.removeItem(SESSION_STORAGE_KEY);
241
+ }
242
+ }, [enableCache]);
243
+ return {
244
+ messages,
245
+ add,
246
+ cancel,
247
+ setMessages,
248
+ clearHistory,
249
+ isLoadingHistory,
250
+ };
100
251
  }
@@ -92,11 +92,13 @@ function ImagePreview({ dataSource = [], itemWidth = undefined, itemHeight = und
92
92
  opacity: 1,
93
93
  },
94
94
  },
95
- }, children: (0, jsx_runtime_1.jsxs)(material_1.Box, { sx: { position: 'relative' }, children: [(0, jsx_runtime_1.jsx)(react_photo_view_1.PhotoView, { ...item, children: (0, jsx_runtime_1.jsx)(loading_image_1.default, { ...item, src: (0, ufo_1.withQuery)(item.src, {
96
- imageFilter: 'resize',
97
- f: 'webp',
98
- w: typeof itemWidth === 'number' ? Math.min(itemWidth * 2, 1200) : undefined,
99
- }), style: {
95
+ }, children: (0, jsx_runtime_1.jsxs)(material_1.Box, { sx: { position: 'relative' }, children: [(0, jsx_runtime_1.jsx)(react_photo_view_1.PhotoView, { ...item, children: (0, jsx_runtime_1.jsx)(loading_image_1.default, { ...item, src: item.src.startsWith('data:')
96
+ ? item.src
97
+ : (0, ufo_1.withQuery)(item.src, {
98
+ imageFilter: 'resize',
99
+ f: 'webp',
100
+ w: typeof itemWidth === 'number' ? Math.min(itemWidth * 2, 1200) : undefined,
101
+ }), style: {
100
102
  transition,
101
103
  borderRadius,
102
104
  objectFit: 'cover',
@@ -1,3 +1,4 @@
1
1
  export * from './chat';
2
2
  export * from './image';
3
3
  export * from './embedding';
4
+ export * from './model';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Avatar, Box, CircularProgress } from '@mui/material';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Avatar, Box, CircularProgress, Fade } from '@mui/material';
3
3
  import isNil from 'lodash/isNil';
4
4
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
5
5
  import CreditErrorAlert from '../credit/alert';
6
6
  import ImagePreview from '../image-preview';
7
7
  import Message from './message';
8
8
  import Prompt from './prompt';
9
- export default function Conversation({ ref, messages, onSubmit, customActions = () => [], renderAvatar = undefined, maxWidth = 1000, scrollContainer = undefined, promptProps = {}, ...props }) {
9
+ export default function Conversation({ ref, messages, onSubmit, customActions = () => [], renderAvatar = undefined, maxWidth = 1000, scrollContainer = undefined, promptProps = {}, chatLayout = 'left-right', ...props }) {
10
10
  const scroller = useRef(scrollContainer !== null && scrollContainer !== void 0 ? scrollContainer : null);
11
11
  const { element, scrollToBottom } = useAutoScrollToBottom({ scroller });
12
12
  useImperativeHandle(ref, () => ({
@@ -18,28 +18,57 @@ export default function Conversation({ ref, messages, onSubmit, customActions =
18
18
  flexDirection: 'column',
19
19
  overflow: 'auto',
20
20
  ...props.sx,
21
- }, children: _jsxs(Box, { sx: { mt: 2, mx: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }, className: "conversation-container", children: [_jsxs(Box, { sx: { flexGrow: 1, width: '100%', mx: 'auto', maxWidth }, children: [messages.map((msg) => {
21
+ }, children: _jsxs(Box, { sx: { mt: 3, mx: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }, className: "conversation-container", children: [_jsxs(Box, { sx: { flexGrow: 1, width: '100%', mx: 'auto', maxWidth }, children: [messages.map((msg) => {
22
22
  var _a, _b;
23
23
  const actions = customActions === null || customActions === void 0 ? void 0 : customActions(msg);
24
- return (_jsxs(Box, { id: `conversation-${msg.id}`, children: [!isNil(msg.prompt) && (_jsx(Message, { avatar: (_a = renderAvatar === null || renderAvatar === void 0 ? void 0 : renderAvatar(msg, false)) !== null && _a !== void 0 ? _a : _jsx(Avatar, { sx: { bgcolor: 'secondary.main' }, children: "\uD83E\uDDD1" }), message: msg.prompt, actions: actions === null || actions === void 0 ? void 0 : actions[0] })), (!isNil(msg.response) || !isNil(msg.loading) || !isNil(msg.error)) && (_jsxs(Message, { my: 1, id: `response-${msg.id}`, loading: msg.loading && !!msg.response, message: typeof msg.response === 'string' ? msg.response : undefined, avatar: (_b = renderAvatar === null || renderAvatar === void 0 ? void 0 : renderAvatar(msg, true)) !== null && _b !== void 0 ? _b : _jsx(Avatar, { sx: { bgcolor: 'primary.main' }, children: "\uD83E\uDD16\uFE0F" }), actions: actions === null || actions === void 0 ? void 0 : actions[1], children: [Array.isArray(msg.response) && (_jsx(ImagePreview, { itemWidth: 100, dataSource: msg.response.map(({ url }) => {
25
- return {
26
- src: url,
27
- onLoad: () => scrollToBottom(),
28
- };
29
- }) })), msg.error ? (
30
- // @ts-ignore
31
- _jsx(CreditErrorAlert, { error: msg.error })) : (msg.loading &&
32
- !msg.response && (_jsx(Box, { sx: {
33
- minHeight: 24,
34
- display: 'flex',
35
- alignItems: 'center',
36
- }, children: _jsx(CircularProgress, { size: 16 }) })))] }))] }, msg.id));
24
+ const isLeftRight = chatLayout === 'left-right';
25
+ return (_jsx(Fade, { in: true, timeout: 300, children: _jsxs(Box, { id: `conversation-${msg.id}`, children: [!isNil(msg.prompt) && (_jsx(Message, { avatar: (_a = renderAvatar === null || renderAvatar === void 0 ? void 0 : renderAvatar(msg, false)) !== null && _a !== void 0 ? _a : _jsx(Avatar, { sx: { bgcolor: 'secondary.main' }, children: "\uD83E\uDDD1" }), message: msg.prompt, actions: actions === null || actions === void 0 ? void 0 : actions[0], timestamp: msg.timestamp, isUser: isLeftRight, chatLayout: chatLayout })), (!isNil(msg.response) || !isNil(msg.loading) || !isNil(msg.error)) && (_jsxs(Message, { id: `response-${msg.id}`, loading: msg.loading && !!msg.response, message: typeof msg.response === 'string' ? msg.response : undefined, avatar: (_b = renderAvatar === null || renderAvatar === void 0 ? void 0 : renderAvatar(msg, true)) !== null && _b !== void 0 ? _b : _jsx(Avatar, { sx: { bgcolor: 'primary.main' }, children: "\uD83E\uDD16\uFE0F" }), actions: actions === null || actions === void 0 ? void 0 : actions[1], timestamp: msg.timestamp, isUser: false, chatLayout: chatLayout, children: [msg.response &&
26
+ typeof msg.response === 'object' &&
27
+ 'images' in msg.response &&
28
+ Array.isArray(msg.response.images) &&
29
+ msg.response.images.length > 0 && (_jsxs(_Fragment, { children: [msg.response.images.some((img) => img.url && img.url.startsWith('data:')) && (_jsx(ImagePreview, { itemWidth: 200, borderRadius: 12, dataSource: msg.response.images
30
+ .filter((img) => img.url && img.url.startsWith('data:'))
31
+ .map(({ url }) => ({
32
+ src: url,
33
+ onLoad: () => scrollToBottom(),
34
+ })) })), msg.response.images.some((img) => !img.url || img.url === '[IMAGE_PLACEHOLDER]') && (_jsxs(Box, { sx: {
35
+ margin: '8px 0',
36
+ minHeight: '200px',
37
+ background: '#f5f5f5',
38
+ borderRadius: '8px',
39
+ padding: '16px',
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ justifyContent: 'center',
43
+ flexDirection: 'column',
44
+ gap: '12px',
45
+ border: '2px dashed #ddd',
46
+ }, children: [_jsx(Box, { sx: { fontSize: '48px', opacity: 0.4 }, children: "\uD83D\uDDBC\uFE0F" }), _jsx(Box, { sx: {
47
+ fontSize: '14px',
48
+ color: '#666',
49
+ textAlign: 'center',
50
+ fontWeight: 500,
51
+ minWidth: 200,
52
+ }, children: msg.response.images.filter((img) => !img.url || img.url === '[IMAGE_PLACEHOLDER]')
53
+ .length === 1
54
+ ? 'Image (Not Cached)'
55
+ : `${msg.response.images.filter((img) => !img.url || img.url === '[IMAGE_PLACEHOLDER]').length} Images (Not Cached)` })] }))] })), msg.error ? (
56
+ // @ts-ignore
57
+ _jsx(CreditErrorAlert, { error: msg.error })) : (msg.loading &&
58
+ !msg.response && (_jsxs(Box, { sx: {
59
+ minHeight: 32,
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ gap: 1.5,
63
+ color: 'text.secondary',
64
+ fontSize: '14px',
65
+ }, children: [_jsx(CircularProgress, { size: 18, thickness: 4 }), _jsx("span", { children: "AI is thinking..." })] })))] }))] }) }, msg.id));
37
66
  }), element] }), _jsxs(Box, { sx: { mx: 'auto', width: '100%', maxWidth, position: 'sticky', bottom: 0 }, children: [_jsx(Box, { sx: {
38
- height: 16,
67
+ height: 24,
39
68
  pointerEvents: 'none',
40
69
  background: (theme) => `linear-gradient(transparent, ${theme.palette.background.paper})`,
41
70
  } }), _jsx(Box, { sx: {
42
- pb: 2,
71
+ pb: 4,
43
72
  bgcolor: 'background.paper',
44
73
  }, children: _jsx(Prompt, { onSubmit: onSubmit, ...promptProps }) })] })] }) }));
45
74
  }